summaryrefslogtreecommitdiff
path: root/src/com/rafa_99/pkrandom/romhandlers/AbstractRomHandler.java
diff options
context:
space:
mode:
Diffstat (limited to 'src/com/rafa_99/pkrandom/romhandlers/AbstractRomHandler.java')
-rwxr-xr-xsrc/com/rafa_99/pkrandom/romhandlers/AbstractRomHandler.java6761
1 files changed, 6761 insertions, 0 deletions
diff --git a/src/com/rafa_99/pkrandom/romhandlers/AbstractRomHandler.java b/src/com/rafa_99/pkrandom/romhandlers/AbstractRomHandler.java
new file mode 100755
index 0000000..7497d7a
--- /dev/null
+++ b/src/com/rafa_99/pkrandom/romhandlers/AbstractRomHandler.java
@@ -0,0 +1,6761 @@
+package com.rafa_99.pkrandom.romhandlers;
+
+/*----------------------------------------------------------------------------*/
+/*-- AbstractRomHandler.java - a base class for all rom handlers which --*/
+/*-- implements the majority of the actual --*/
+/*-- randomizer logic by building on the base --*/
+/*-- getters & setters provided by each concrete --*/
+/*-- handler. --*/
+/*-- --*/
+/*-- Part of "Universal Pokemon Randomizer ZX" by the UPR-ZX team --*/
+/*-- Originally part of "Universal Pokemon Randomizer" by rafa_99 --*/
+/*-- Pokemon and any associated names and the like are --*/
+/*-- trademark and (C) Nintendo 1996-2020. --*/
+/*-- --*/
+/*-- The custom code written here is licensed under the terms of the GPL: --*/
+/*-- --*/
+/*-- This program is free software: you can redistribute it and/or modify --*/
+/*-- it under the terms of the GNU General Public License as published by --*/
+/*-- the Free Software Foundation, either version 3 of the License, or --*/
+/*-- (at your option) any later version. --*/
+/*-- --*/
+/*-- This program is distributed in the hope that it will be useful, --*/
+/*-- but WITHOUT ANY WARRANTY; without even the implied warranty of --*/
+/*-- MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the --*/
+/*-- GNU General Public License for more details. --*/
+/*-- --*/
+/*-- You should have received a copy of the GNU General Public License --*/
+/*-- along with this program. If not, see <http://www.gnu.org/licenses/>. --*/
+/*----------------------------------------------------------------------------*/
+
+import java.io.PrintStream;
+import java.util.*;
+import java.util.stream.Collectors;
+
+import com.rafa_99.pkrandom.*;
+import com.rafa_99.pkrandom.constants.*;
+import com.rafa_99.pkrandom.exceptions.RandomizationException;
+import com.rafa_99.pkrandom.pokemon.*;
+
+public abstract class AbstractRomHandler implements RomHandler {
+
+ private boolean restrictionsSet;
+ protected List<Pokemon> mainPokemonList;
+ protected List<Pokemon> mainPokemonListInclFormes;
+ private List<Pokemon> altFormesList;
+ private List<MegaEvolution> megaEvolutionsList;
+ private List<Pokemon> noLegendaryList, onlyLegendaryList, ultraBeastList;
+ private List<Pokemon> noLegendaryListInclFormes, onlyLegendaryListInclFormes;
+ private List<Pokemon> noLegendaryAltsList, onlyLegendaryAltsList;
+ private List<Pokemon> pickedStarters;
+ protected final Random random;
+ private final Random cosmeticRandom;
+ protected PrintStream logStream;
+ private List<Pokemon> alreadyPicked = new ArrayList<>();
+ private Map<Pokemon, Integer> placementHistory = new HashMap<>();
+ private Map<Integer, Integer> itemPlacementHistory = new HashMap<>();
+ boolean isORAS = false;
+ boolean isSM = false;
+ int perfectAccuracy = 100;
+
+ /* Constructor */
+
+ public AbstractRomHandler(Random random, PrintStream logStream) {
+ this.random = random;
+ this.cosmeticRandom = RandomSource.cosmeticInstance();
+ this.logStream = logStream;
+ }
+
+ /*
+ * Public Methods, implemented here for all gens. Unlikely to be overridden.
+ */
+
+ public void setLog(PrintStream logStream) {
+ this.logStream = logStream;
+ }
+
+ public void setPokemonPool(Settings settings) {
+ GenRestrictions restrictions = null;
+ if (settings != null) {
+ restrictions = settings.getCurrentRestrictions();
+
+ // restrictions should already be null if "Limit Pokemon" is disabled, but this is a safeguard
+ if (!settings.isLimitPokemon()) {
+ restrictions = null;
+ }
+ }
+
+ restrictionsSet = true;
+ mainPokemonList = this.allPokemonWithoutNull();
+ mainPokemonListInclFormes = this.allPokemonInclFormesWithoutNull();
+ altFormesList = this.getAltFormes();
+ megaEvolutionsList = this.getMegaEvolutions();
+ if (restrictions != null) {
+ mainPokemonList = new ArrayList<>();
+ mainPokemonListInclFormes = new ArrayList<>();
+ megaEvolutionsList = new ArrayList<>();
+ List<Pokemon> allPokemon = this.getPokemon();
+
+ if (restrictions.allow_gen1) {
+ addPokesFromRange(mainPokemonList, allPokemon, Species.bulbasaur, Species.mew);
+ }
+
+ if (restrictions.allow_gen2 && allPokemon.size() > Gen2Constants.pokemonCount) {
+ addPokesFromRange(mainPokemonList, allPokemon, Species.chikorita, Species.celebi);
+ }
+
+ if (restrictions.allow_gen3 && allPokemon.size() > Gen3Constants.pokemonCount) {
+ addPokesFromRange(mainPokemonList, allPokemon, Species.treecko, Species.deoxys);
+ }
+
+ if (restrictions.allow_gen4 && allPokemon.size() > Gen4Constants.pokemonCount) {
+ addPokesFromRange(mainPokemonList, allPokemon, Species.turtwig, Species.arceus);
+ }
+
+ if (restrictions.allow_gen5 && allPokemon.size() > Gen5Constants.pokemonCount) {
+ addPokesFromRange(mainPokemonList, allPokemon, Species.victini, Species.genesect);
+ }
+
+ if (restrictions.allow_gen6 && allPokemon.size() > Gen6Constants.pokemonCount) {
+ addPokesFromRange(mainPokemonList, allPokemon, Species.chespin, Species.volcanion);
+ }
+
+ int maxGen7SpeciesID = isSM ? Species.marshadow : Species.zeraora;
+ if (restrictions.allow_gen7 && allPokemon.size() > maxGen7SpeciesID) {
+ addPokesFromRange(mainPokemonList, allPokemon, Species.rowlet, maxGen7SpeciesID);
+ }
+
+ // If the user specified it, add all the evolutionary relatives for everything in the mainPokemonList
+ if (restrictions.allow_evolutionary_relatives) {
+ addEvolutionaryRelatives(mainPokemonList);
+ }
+
+ // Now that mainPokemonList has all the selected Pokemon, update mainPokemonListInclFormes too
+ addAllPokesInclFormes(mainPokemonList, mainPokemonListInclFormes);
+
+ // Populate megaEvolutionsList with all of the mega evolutions that exist in the pool
+ List<MegaEvolution> allMegaEvolutions = this.getMegaEvolutions();
+ for (MegaEvolution megaEvo : allMegaEvolutions) {
+ if (mainPokemonListInclFormes.contains(megaEvo.to)) {
+ megaEvolutionsList.add(megaEvo);
+ }
+ }
+ }
+
+ noLegendaryList = new ArrayList<>();
+ noLegendaryListInclFormes = new ArrayList<>();
+ onlyLegendaryList = new ArrayList<>();
+ onlyLegendaryListInclFormes = new ArrayList<>();
+ noLegendaryAltsList = new ArrayList<>();
+ onlyLegendaryAltsList = new ArrayList<>();
+ ultraBeastList = new ArrayList<>();
+
+ for (Pokemon p : mainPokemonList) {
+ if (p.isLegendary()) {
+ onlyLegendaryList.add(p);
+ } else if (p.isUltraBeast()) {
+ ultraBeastList.add(p);
+ } else {
+ noLegendaryList.add(p);
+ }
+ }
+ for (Pokemon p : mainPokemonListInclFormes) {
+ if (p.isLegendary()) {
+ onlyLegendaryListInclFormes.add(p);
+ } else if (!ultraBeastList.contains(p)) {
+ noLegendaryListInclFormes.add(p);
+ }
+ }
+ for (Pokemon f : altFormesList) {
+ if (f.isLegendary()) {
+ onlyLegendaryAltsList.add(f);
+ } else {
+ noLegendaryAltsList.add(f);
+ }
+ }
+ }
+
+ private void addPokesFromRange(List<Pokemon> pokemonPool, List<Pokemon> allPokemon, int range_min, int range_max) {
+ for (int i = range_min; i <= range_max; i++) {
+ if (!pokemonPool.contains(allPokemon.get(i))) {
+ pokemonPool.add(allPokemon.get(i));
+ }
+ }
+ }
+
+ private void addEvolutionaryRelatives(List<Pokemon> pokemonPool) {
+ Set<Pokemon> newPokemon = new TreeSet<>();
+ for (Pokemon pk : pokemonPool) {
+ List<Pokemon> evolutionaryRelatives = getEvolutionaryRelatives(pk);
+ for (Pokemon relative : evolutionaryRelatives) {
+ if (!pokemonPool.contains(relative) && !newPokemon.contains(relative)) {
+ newPokemon.add(relative);
+ }
+ }
+ }
+
+ pokemonPool.addAll(newPokemon);
+ }
+
+ private void addAllPokesInclFormes(List<Pokemon> pokemonPool, List<Pokemon> pokemonPoolInclFormes) {
+ List<Pokemon> altFormes = this.getAltFormes();
+ for (int i = 0; i < pokemonPool.size(); i++) {
+ Pokemon currentPokemon = pokemonPool.get(i);
+ if (!pokemonPoolInclFormes.contains(currentPokemon)) {
+ pokemonPoolInclFormes.add(currentPokemon);
+ }
+ for (int j = 0; j < altFormes.size(); j++) {
+ Pokemon potentialAltForme = altFormes.get(j);
+ if (potentialAltForme.baseForme != null && potentialAltForme.baseForme.number == currentPokemon.number) {
+ pokemonPoolInclFormes.add(potentialAltForme);
+ }
+ }
+ }
+ }
+
+ @Override
+ public void shufflePokemonStats(Settings settings) {
+ boolean evolutionSanity = settings.isBaseStatsFollowEvolutions();
+ boolean megaEvolutionSanity = settings.isBaseStatsFollowMegaEvolutions();
+
+ if (evolutionSanity) {
+ copyUpEvolutionsHelper(pk -> pk.shuffleStats(AbstractRomHandler.this.random),
+ (evFrom, evTo, toMonIsFinalEvo) -> evTo.copyShuffledStatsUpEvolution(evFrom)
+ );
+ } else {
+ List<Pokemon> allPokes = this.getPokemonInclFormes();
+ for (Pokemon pk : allPokes) {
+ if (pk != null) {
+ pk.shuffleStats(this.random);
+ }
+ }
+ }
+
+ List<Pokemon> allPokes = this.getPokemonInclFormes();
+ for (Pokemon pk : allPokes) {
+ if (pk != null && pk.actuallyCosmetic) {
+ pk.copyBaseFormeBaseStats(pk.baseForme);
+ }
+ }
+
+ if (megaEvolutionSanity) {
+ List<MegaEvolution> allMegaEvos = getMegaEvolutions();
+ for (MegaEvolution megaEvo: allMegaEvos) {
+ if (megaEvo.from.megaEvolutionsFrom.size() > 1) continue;
+ megaEvo.to.copyShuffledStatsUpEvolution(megaEvo.from);
+ }
+ }
+ }
+
+ @Override
+ public void randomizePokemonStats(Settings settings) {
+ boolean evolutionSanity = settings.isBaseStatsFollowEvolutions();
+ boolean megaEvolutionSanity = settings.isBaseStatsFollowMegaEvolutions();
+ boolean assignEvoStatsRandomly = settings.isAssignEvoStatsRandomly();
+
+ if (evolutionSanity) {
+ if (assignEvoStatsRandomly) {
+ copyUpEvolutionsHelper(pk -> pk.randomizeStatsWithinBST(AbstractRomHandler.this.random),
+ (evFrom, evTo, toMonIsFinalEvo) -> evTo.assignNewStatsForEvolution(evFrom, this.random),
+ (evFrom, evTo, toMonIsFinalEvo) -> evTo.assignNewStatsForEvolution(evFrom, this.random),
+ true
+ );
+ } else {
+ copyUpEvolutionsHelper(pk -> pk.randomizeStatsWithinBST(AbstractRomHandler.this.random),
+ (evFrom, evTo, toMonIsFinalEvo) -> evTo.copyRandomizedStatsUpEvolution(evFrom),
+ (evFrom, evTo, toMonIsFinalEvo) -> evTo.assignNewStatsForEvolution(evFrom, this.random),
+ true
+ );
+ }
+ } else {
+ List<Pokemon> allPokes = this.getPokemonInclFormes();
+ for (Pokemon pk : allPokes) {
+ if (pk != null) {
+ pk.randomizeStatsWithinBST(this.random);
+ }
+ }
+ }
+
+ List<Pokemon> allPokes = this.getPokemonInclFormes();
+ for (Pokemon pk : allPokes) {
+ if (pk != null && pk.actuallyCosmetic) {
+ pk.copyBaseFormeBaseStats(pk.baseForme);
+ }
+ }
+
+ if (megaEvolutionSanity) {
+ List<MegaEvolution> allMegaEvos = getMegaEvolutions();
+ for (MegaEvolution megaEvo: allMegaEvos) {
+ if (megaEvo.from.megaEvolutionsFrom.size() > 1 || assignEvoStatsRandomly) {
+ megaEvo.to.assignNewStatsForEvolution(megaEvo.from, this.random);
+ } else {
+ megaEvo.to.copyRandomizedStatsUpEvolution(megaEvo.from);
+ }
+ }
+ }
+ }
+
+ @Override
+ public void updatePokemonStats(Settings settings) {
+ int generation = settings.getUpdateBaseStatsToGeneration();
+
+ List<Pokemon> pokes = getPokemonInclFormes();
+
+ for (int gen = 6; gen <= generation; gen++) {
+ Map<Integer,StatChange> statChanges = getUpdatedPokemonStats(gen);
+
+ for (int i = 1; i < pokes.size(); i++) {
+ StatChange changedStats = statChanges.get(i);
+ if (changedStats != null) {
+ int statNum = 0;
+ if ((changedStats.stat & Stat.HP.val) != 0) {
+ pokes.get(i).hp = changedStats.values[statNum];
+ statNum++;
+ }
+ if ((changedStats.stat & Stat.ATK.val) != 0) {
+ pokes.get(i).attack = changedStats.values[statNum];
+ statNum++;
+ }
+ if ((changedStats.stat & Stat.DEF.val) != 0) {
+ pokes.get(i).defense = changedStats.values[statNum];
+ statNum++;
+ }
+ if ((changedStats.stat & Stat.SPATK.val) != 0) {
+ if (generationOfPokemon() != 1) {
+ pokes.get(i).spatk = changedStats.values[statNum];
+ }
+ statNum++;
+ }
+ if ((changedStats.stat & Stat.SPDEF.val) != 0) {
+ if (generationOfPokemon() != 1) {
+ pokes.get(i).spdef = changedStats.values[statNum];
+ }
+ statNum++;
+ }
+ if ((changedStats.stat & Stat.SPEED.val) != 0) {
+ pokes.get(i).speed = changedStats.values[statNum];
+ statNum++;
+ }
+ if ((changedStats.stat & Stat.SPECIAL.val) != 0) {
+ pokes.get(i).special = changedStats.values[statNum];
+ }
+ }
+ }
+ }
+ }
+
+ public Pokemon randomPokemon() {
+ checkPokemonRestrictions();
+ return mainPokemonList.get(this.random.nextInt(mainPokemonList.size()));
+ }
+
+ @Override
+ public Pokemon randomPokemonInclFormes() {
+ checkPokemonRestrictions();
+ return mainPokemonListInclFormes.get(this.random.nextInt(mainPokemonListInclFormes.size()));
+ }
+
+ @Override
+ public Pokemon randomNonLegendaryPokemon() {
+ checkPokemonRestrictions();
+ return noLegendaryList.get(this.random.nextInt(noLegendaryList.size()));
+ }
+
+ private Pokemon randomNonLegendaryPokemonInclFormes() {
+ checkPokemonRestrictions();
+ return noLegendaryListInclFormes.get(this.random.nextInt(noLegendaryListInclFormes.size()));
+ }
+
+ @Override
+ public Pokemon randomLegendaryPokemon() {
+ checkPokemonRestrictions();
+ return onlyLegendaryList.get(this.random.nextInt(onlyLegendaryList.size()));
+ }
+
+ private List<Pokemon> twoEvoPokes;
+
+ @Override
+ public Pokemon random2EvosPokemon(boolean allowAltFormes) {
+ if (twoEvoPokes == null) {
+ // Prepare the list
+ twoEvoPokes = new ArrayList<>();
+ List<Pokemon> allPokes =
+ allowAltFormes ?
+ this.getPokemonInclFormes()
+ .stream()
+ .filter(pk -> pk == null || !pk.actuallyCosmetic)
+ .collect(Collectors.toList()) :
+ this.getPokemon();
+ for (Pokemon pk : allPokes) {
+ if (pk != null && pk.evolutionsTo.size() == 0 && pk.evolutionsFrom.size() > 0) {
+ // Potential candidate
+ for (Evolution ev : pk.evolutionsFrom) {
+ // If any of the targets here evolve, the original
+ // Pokemon has 2+ stages.
+ if (ev.to.evolutionsFrom.size() > 0) {
+ twoEvoPokes.add(pk);
+ break;
+ }
+ }
+ }
+ }
+ }
+ return twoEvoPokes.get(this.random.nextInt(twoEvoPokes.size()));
+ }
+
+ @Override
+ public Type randomType() {
+ Type t = Type.randomType(this.random);
+ while (!typeInGame(t)) {
+ t = Type.randomType(this.random);
+ }
+ return t;
+ }
+
+ @Override
+ public void randomizePokemonTypes(Settings settings) {
+ boolean evolutionSanity = settings.getTypesMod() == Settings.TypesMod.RANDOM_FOLLOW_EVOLUTIONS;
+ boolean megaEvolutionSanity = settings.isTypesFollowMegaEvolutions();
+ boolean dualTypeOnly = settings.isDualTypeOnly();
+
+ List<Pokemon> allPokes = this.getPokemonInclFormes();
+ if (evolutionSanity) {
+ // Type randomization with evolution sanity
+ copyUpEvolutionsHelper(pk -> {
+ // Step 1: Basic or Excluded From Copying Pokemon
+ // A Basic/EFC pokemon has a 35% chance of a second type if
+ // it has an evolution that copies type/stats, a 50% chance
+ // otherwise
+ pk.primaryType = randomType();
+ pk.secondaryType = null;
+ if (pk.evolutionsFrom.size() == 1 && pk.evolutionsFrom.get(0).carryStats) {
+ if (AbstractRomHandler.this.random.nextDouble() < 0.35 || dualTypeOnly) {
+ pk.secondaryType = randomType();
+ while (pk.secondaryType == pk.primaryType) {
+ pk.secondaryType = randomType();
+ }
+ }
+ } else {
+ if (AbstractRomHandler.this.random.nextDouble() < 0.5 || dualTypeOnly) {
+ pk.secondaryType = randomType();
+ while (pk.secondaryType == pk.primaryType) {
+ pk.secondaryType = randomType();
+ }
+ }
+ }
+ }, (evFrom, evTo, toMonIsFinalEvo) -> {
+ evTo.primaryType = evFrom.primaryType;
+ evTo.secondaryType = evFrom.secondaryType;
+
+ if (evTo.secondaryType == null) {
+ double chance = toMonIsFinalEvo ? 0.25 : 0.15;
+ if (AbstractRomHandler.this.random.nextDouble() < chance || dualTypeOnly) {
+ evTo.secondaryType = randomType();
+ while (evTo.secondaryType == evTo.primaryType) {
+ evTo.secondaryType = randomType();
+ }
+ }
+ }
+ });
+ } else {
+ // Entirely random types
+ for (Pokemon pkmn : allPokes) {
+ if (pkmn != null) {
+ pkmn.primaryType = randomType();
+ pkmn.secondaryType = null;
+ if (this.random.nextDouble() < 0.5||settings.isDualTypeOnly()) {
+ pkmn.secondaryType = randomType();
+ while (pkmn.secondaryType == pkmn.primaryType) {
+ pkmn.secondaryType = randomType();
+ }
+ }
+ }
+ }
+ }
+
+ for (Pokemon pk : allPokes) {
+ if (pk != null && pk.actuallyCosmetic) {
+ pk.primaryType = pk.baseForme.primaryType;
+ pk.secondaryType = pk.baseForme.secondaryType;
+ }
+ }
+
+ if (megaEvolutionSanity) {
+ List<MegaEvolution> allMegaEvos = getMegaEvolutions();
+ for (MegaEvolution megaEvo: allMegaEvos) {
+ if (megaEvo.from.megaEvolutionsFrom.size() > 1) continue;
+ megaEvo.to.primaryType = megaEvo.from.primaryType;
+ megaEvo.to.secondaryType = megaEvo.from.secondaryType;
+
+ if (megaEvo.to.secondaryType == null) {
+ if (this.random.nextDouble() < 0.25) {
+ megaEvo.to.secondaryType = randomType();
+ while (megaEvo.to.secondaryType == megaEvo.to.primaryType) {
+ megaEvo.to.secondaryType = randomType();
+ }
+ }
+ }
+ }
+ }
+ }
+
+ @Override
+ public void randomizeAbilities(Settings settings) {
+ boolean evolutionSanity = settings.isAbilitiesFollowEvolutions();
+ boolean allowWonderGuard = settings.isAllowWonderGuard();
+ boolean banTrappingAbilities = settings.isBanTrappingAbilities();
+ boolean banNegativeAbilities = settings.isBanNegativeAbilities();
+ boolean banBadAbilities = settings.isBanBadAbilities();
+ boolean megaEvolutionSanity = settings.isAbilitiesFollowMegaEvolutions();
+ boolean weighDuplicatesTogether = settings.isWeighDuplicateAbilitiesTogether();
+ boolean doubleBattleMode = settings.isDoubleBattleMode();
+
+ // Abilities don't exist in some games...
+ if (this.abilitiesPerPokemon() == 0) {
+ return;
+ }
+
+ final boolean hasDWAbilities = (this.abilitiesPerPokemon() == 3);
+
+ final List<Integer> bannedAbilities = this.getUselessAbilities();
+
+ if (!allowWonderGuard) {
+ bannedAbilities.add(Abilities.wonderGuard);
+ }
+
+ if (banTrappingAbilities) {
+ bannedAbilities.addAll(GlobalConstants.battleTrappingAbilities);
+ }
+
+ if (banNegativeAbilities) {
+ bannedAbilities.addAll(GlobalConstants.negativeAbilities);
+ }
+
+ if (banBadAbilities) {
+ bannedAbilities.addAll(GlobalConstants.badAbilities);
+ if (!doubleBattleMode) {
+ bannedAbilities.addAll(GlobalConstants.doubleBattleAbilities);
+ }
+ }
+
+ if (weighDuplicatesTogether) {
+ bannedAbilities.addAll(GlobalConstants.duplicateAbilities);
+ if (generationOfPokemon() == 3) {
+ bannedAbilities.add(Gen3Constants.airLockIndex); // Special case for Air Lock in Gen 3
+ }
+ }
+
+ final int maxAbility = this.highestAbilityIndex();
+
+ if (evolutionSanity) {
+ // copy abilities straight up evolution lines
+ // still keep WG as an exception, though
+
+ copyUpEvolutionsHelper(pk -> {
+ if (pk.ability1 != Abilities.wonderGuard
+ && pk.ability2 != Abilities.wonderGuard
+ && pk.ability3 != Abilities.wonderGuard) {
+ // Pick first ability
+ pk.ability1 = pickRandomAbility(maxAbility, bannedAbilities, weighDuplicatesTogether);
+
+ // Second ability?
+ if (AbstractRomHandler.this.random.nextDouble() < 0.5) {
+ // Yes, second ability
+ pk.ability2 = pickRandomAbility(maxAbility, bannedAbilities, weighDuplicatesTogether,
+ pk.ability1);
+ } else {
+ // Nope
+ pk.ability2 = 0;
+ }
+
+ // Third ability?
+ if (hasDWAbilities) {
+ pk.ability3 = pickRandomAbility(maxAbility, bannedAbilities, weighDuplicatesTogether,
+ pk.ability1, pk.ability2);
+ }
+ }
+ }, (evFrom, evTo, toMonIsFinalEvo) -> {
+ if (evTo.ability1 != Abilities.wonderGuard
+ && evTo.ability2 != Abilities.wonderGuard
+ && evTo.ability3 != Abilities.wonderGuard) {
+ evTo.ability1 = evFrom.ability1;
+ evTo.ability2 = evFrom.ability2;
+ evTo.ability3 = evFrom.ability3;
+ }
+ });
+ } else {
+ List<Pokemon> allPokes = this.getPokemonInclFormes();
+ for (Pokemon pk : allPokes) {
+ if (pk == null) {
+ continue;
+ }
+
+ // Don't remove WG if already in place.
+ if (pk.ability1 != Abilities.wonderGuard
+ && pk.ability2 != Abilities.wonderGuard
+ && pk.ability3 != Abilities.wonderGuard) {
+ // Pick first ability
+ pk.ability1 = this.pickRandomAbility(maxAbility, bannedAbilities, weighDuplicatesTogether);
+
+ // Second ability?
+ if (this.random.nextDouble() < 0.5) {
+ // Yes, second ability
+ pk.ability2 = this.pickRandomAbility(maxAbility, bannedAbilities, weighDuplicatesTogether,
+ pk.ability1);
+ } else {
+ // Nope
+ pk.ability2 = 0;
+ }
+
+ // Third ability?
+ if (hasDWAbilities) {
+ pk.ability3 = pickRandomAbility(maxAbility, bannedAbilities, weighDuplicatesTogether,
+ pk.ability1, pk.ability2);
+ }
+ }
+ }
+ }
+
+ List<Pokemon> allPokes = this.getPokemonInclFormes();
+ for (Pokemon pk : allPokes) {
+ if (pk != null && pk.actuallyCosmetic) {
+ pk.copyBaseFormeAbilities(pk.baseForme);
+ }
+ }
+
+ if (megaEvolutionSanity) {
+ List<MegaEvolution> allMegaEvos = this.getMegaEvolutions();
+ for (MegaEvolution megaEvo: allMegaEvos) {
+ if (megaEvo.from.megaEvolutionsFrom.size() > 1) continue;
+ megaEvo.to.ability1 = megaEvo.from.ability1;
+ megaEvo.to.ability2 = megaEvo.from.ability2;
+ megaEvo.to.ability3 = megaEvo.from.ability3;
+ }
+ }
+ }
+
+ private int pickRandomAbility(int maxAbility, List<Integer> bannedAbilities, boolean useVariations,
+ int... alreadySetAbilities) {
+ int newAbility;
+
+ while (true) {
+ newAbility = this.random.nextInt(maxAbility) + 1;
+
+ if (bannedAbilities.contains(newAbility)) {
+ continue;
+ }
+
+ boolean repeat = false;
+ for (int alreadySetAbility : alreadySetAbilities) {
+ if (alreadySetAbility == newAbility) {
+ repeat = true;
+ break;
+ }
+ }
+
+ if (!repeat) {
+ if (useVariations) {
+ Map<Integer,List<Integer>> abilityVariations = getAbilityVariations();
+ for (int baseAbility: abilityVariations.keySet()) {
+ if (newAbility == baseAbility) {
+ List<Integer> variationsForThisAbility = abilityVariations.get(newAbility);
+ newAbility = variationsForThisAbility.get(this.random.nextInt(variationsForThisAbility.size()));
+ break;
+ }
+ }
+ }
+ break;
+ }
+ }
+
+ return newAbility;
+ }
+
+ @Override
+ public void randomEncounters(Settings settings) {
+ boolean useTimeOfDay = settings.isUseTimeBasedEncounters();
+ boolean catchEmAll = settings.getWildPokemonRestrictionMod() == Settings.WildPokemonRestrictionMod.CATCH_EM_ALL;
+ boolean typeThemed = settings.getWildPokemonRestrictionMod() == Settings.WildPokemonRestrictionMod.TYPE_THEME_AREAS;
+ boolean usePowerLevels = settings.getWildPokemonRestrictionMod() == Settings.WildPokemonRestrictionMod.SIMILAR_STRENGTH;
+ boolean noLegendaries = settings.isBlockWildLegendaries();
+ boolean balanceShakingGrass = settings.isBalanceShakingGrass();
+ int levelModifier = settings.isWildLevelsModified() ? settings.getWildLevelModifier() : 0;
+ boolean allowAltFormes = settings.isAllowWildAltFormes();
+ boolean banIrregularAltFormes = settings.isBanIrregularAltFormes();
+ boolean abilitiesAreRandomized = settings.getAbilitiesMod() == Settings.AbilitiesMod.RANDOMIZE;
+
+ List<EncounterSet> currentEncounters = this.getEncounters(useTimeOfDay);
+
+ if (isORAS) {
+ List<EncounterSet> collapsedEncounters = collapseAreasORAS(currentEncounters);
+ area1to1EncountersImpl(collapsedEncounters, settings);
+ enhanceRandomEncountersORAS(collapsedEncounters, settings);
+ setEncounters(useTimeOfDay, currentEncounters);
+ return;
+ }
+
+ checkPokemonRestrictions();
+
+ // New: randomize the order encounter sets are randomized in.
+ // Leads to less predictable results for various modifiers.
+ // Need to keep the original ordering around for saving though.
+ List<EncounterSet> scrambledEncounters = new ArrayList<>(currentEncounters);
+ Collections.shuffle(scrambledEncounters, this.random);
+
+ List<Pokemon> banned = this.bannedForWildEncounters();
+ banned.addAll(this.getBannedFormesForPlayerPokemon());
+ if (!abilitiesAreRandomized) {
+ List<Pokemon> abilityDependentFormes = getAbilityDependentFormes();
+ banned.addAll(abilityDependentFormes);
+ }
+ if (banIrregularAltFormes) {
+ banned.addAll(getIrregularFormes());
+ }
+ // Assume EITHER catch em all OR type themed OR match strength for now
+ if (catchEmAll) {
+
+ List<Pokemon> allPokes;
+ if (allowAltFormes) {
+ allPokes = noLegendaries ? new ArrayList<>(noLegendaryListInclFormes) : new ArrayList<>(
+ mainPokemonListInclFormes);
+ allPokes.removeIf(o -> ((Pokemon) o).actuallyCosmetic);
+ } else {
+ allPokes = noLegendaries ? new ArrayList<>(noLegendaryList) : new ArrayList<>(
+ mainPokemonList);
+ }
+ allPokes.removeAll(banned);
+
+ for (EncounterSet area : scrambledEncounters) {
+ List<Pokemon> pickablePokemon = allPokes;
+ if (area.bannedPokemon.size() > 0) {
+ pickablePokemon = new ArrayList<>(allPokes);
+ pickablePokemon.removeAll(area.bannedPokemon);
+ }
+ for (Encounter enc : area.encounters) {
+ // Pick a random pokemon
+ if (pickablePokemon.size() == 0) {
+ // Only banned pokes are left, ignore them and pick
+ // something else for now.
+ List<Pokemon> tempPickable;
+ if (allowAltFormes) {
+ tempPickable = noLegendaries ? new ArrayList<>(noLegendaryListInclFormes) : new ArrayList<>(
+ mainPokemonListInclFormes);
+ tempPickable.removeIf(o -> ((Pokemon) o).actuallyCosmetic);
+ } else {
+ tempPickable = noLegendaries ? new ArrayList<>(noLegendaryList) : new ArrayList<>(
+ mainPokemonList);
+ }
+ tempPickable.removeAll(banned);
+ tempPickable.removeAll(area.bannedPokemon);
+ if (tempPickable.size() == 0) {
+ throw new RandomizationException("ERROR: Couldn't replace a wild Pokemon!");
+ }
+ int picked = this.random.nextInt(tempPickable.size());
+ enc.pokemon = tempPickable.get(picked);
+ setFormeForEncounter(enc, enc.pokemon);
+ } else {
+ // Picked this Pokemon, remove it
+ int picked = this.random.nextInt(pickablePokemon.size());
+ enc.pokemon = pickablePokemon.get(picked);
+ pickablePokemon.remove(picked);
+ if (allPokes != pickablePokemon) {
+ allPokes.remove(enc.pokemon);
+ }
+ setFormeForEncounter(enc, enc.pokemon);
+ if (allPokes.size() == 0) {
+ // Start again
+ if (allowAltFormes) {
+ allPokes.addAll(noLegendaries ? noLegendaryListInclFormes : mainPokemonListInclFormes);
+ allPokes.removeIf(o -> ((Pokemon) o).actuallyCosmetic);
+ } else {
+ allPokes.addAll(noLegendaries ? noLegendaryList : mainPokemonList);
+ }
+ allPokes.removeAll(banned);
+ if (pickablePokemon != allPokes) {
+ pickablePokemon.addAll(allPokes);
+ pickablePokemon.removeAll(area.bannedPokemon);
+ }
+ }
+ }
+ }
+ }
+ } else if (typeThemed) {
+ Map<Type, List<Pokemon>> cachedPokeLists = new TreeMap<>();
+ for (EncounterSet area : scrambledEncounters) {
+ List<Pokemon> possiblePokemon = null;
+ int iterLoops = 0;
+ while (possiblePokemon == null && iterLoops < 10000) {
+ Type areaTheme = randomType();
+ if (!cachedPokeLists.containsKey(areaTheme)) {
+ List<Pokemon> pType = allowAltFormes ? pokemonOfTypeInclFormes(areaTheme, noLegendaries) :
+ pokemonOfType(areaTheme, noLegendaries);
+ pType.removeAll(banned);
+ cachedPokeLists.put(areaTheme, pType);
+ }
+ possiblePokemon = cachedPokeLists.get(areaTheme);
+ if (area.bannedPokemon.size() > 0) {
+ possiblePokemon = new ArrayList<>(possiblePokemon);
+ possiblePokemon.removeAll(area.bannedPokemon);
+ }
+ if (possiblePokemon.size() == 0) {
+ // Can't use this type for this area
+ possiblePokemon = null;
+ }
+ iterLoops++;
+ }
+ if (possiblePokemon == null) {
+ throw new RandomizationException("Could not randomize an area in a reasonable amount of attempts.");
+ }
+ for (Encounter enc : area.encounters) {
+ // Pick a random themed pokemon
+ enc.pokemon = possiblePokemon.get(this.random.nextInt(possiblePokemon.size()));
+ while (enc.pokemon.actuallyCosmetic) {
+ enc.pokemon = possiblePokemon.get(this.random.nextInt(possiblePokemon.size()));
+ }
+ setFormeForEncounter(enc, enc.pokemon);
+ }
+ }
+ } else if (usePowerLevels) {
+ List<Pokemon> allowedPokes;
+ if (allowAltFormes) {
+ allowedPokes = noLegendaries ? new ArrayList<>(noLegendaryListInclFormes)
+ : new ArrayList<>(mainPokemonListInclFormes);
+ } else {
+ allowedPokes = noLegendaries ? new ArrayList<>(noLegendaryList)
+ : new ArrayList<>(mainPokemonList);
+ }
+ allowedPokes.removeAll(banned);
+ for (EncounterSet area : scrambledEncounters) {
+ List<Pokemon> localAllowed = allowedPokes;
+ if (area.bannedPokemon.size() > 0) {
+ localAllowed = new ArrayList<>(allowedPokes);
+ localAllowed.removeAll(area.bannedPokemon);
+ }
+ for (Encounter enc : area.encounters) {
+ if (balanceShakingGrass) {
+ if (area.displayName.contains("Shaking")) {
+ enc.pokemon = pickWildPowerLvlReplacement(localAllowed, enc.pokemon, false, null, (enc.level + enc.maxLevel) / 2);
+ while (enc.pokemon.actuallyCosmetic) {
+ enc.pokemon = pickWildPowerLvlReplacement(localAllowed, enc.pokemon, false, null, (enc.level + enc.maxLevel) / 2);
+ }
+ setFormeForEncounter(enc, enc.pokemon);
+ } else {
+ enc.pokemon = pickWildPowerLvlReplacement(localAllowed, enc.pokemon, false, null, 100);
+ while (enc.pokemon.actuallyCosmetic) {
+ enc.pokemon = pickWildPowerLvlReplacement(localAllowed, enc.pokemon, false, null, 100);
+ }
+ setFormeForEncounter(enc, enc.pokemon);
+ }
+ } else {
+ enc.pokemon = pickWildPowerLvlReplacement(localAllowed, enc.pokemon, false, null, 100);
+ while (enc.pokemon.actuallyCosmetic) {
+ enc.pokemon = pickWildPowerLvlReplacement(localAllowed, enc.pokemon, false, null, 100);
+ }
+ setFormeForEncounter(enc, enc.pokemon);
+ }
+ }
+ }
+ } else {
+ // Entirely random
+ for (EncounterSet area : scrambledEncounters) {
+ for (Encounter enc : area.encounters) {
+ enc.pokemon = pickEntirelyRandomPokemon(allowAltFormes, noLegendaries, area, banned);
+ setFormeForEncounter(enc, enc.pokemon);
+ }
+ }
+ }
+ if (levelModifier != 0) {
+ for (EncounterSet area : currentEncounters) {
+ for (Encounter enc : area.encounters) {
+ enc.level = Math.min(100, (int) Math.round(enc.level * (1 + levelModifier / 100.0)));
+ enc.maxLevel = Math.min(100, (int) Math.round(enc.maxLevel * (1 + levelModifier / 100.0)));
+ }
+ }
+ }
+
+ setEncounters(useTimeOfDay, currentEncounters);
+ }
+
+ @Override
+ public void area1to1Encounters(Settings settings) {
+ boolean useTimeOfDay = settings.isUseTimeBasedEncounters();
+
+ List<EncounterSet> currentEncounters = this.getEncounters(useTimeOfDay);
+ if (isORAS) {
+ List<EncounterSet> collapsedEncounters = collapseAreasORAS(currentEncounters);
+ area1to1EncountersImpl(collapsedEncounters, settings);
+ setEncounters(useTimeOfDay, currentEncounters);
+ return;
+ } else {
+ area1to1EncountersImpl(currentEncounters, settings);
+ setEncounters(useTimeOfDay, currentEncounters);
+ }
+ }
+
+ private void area1to1EncountersImpl(List<EncounterSet> currentEncounters, Settings settings) {
+ boolean catchEmAll = settings.getWildPokemonRestrictionMod() == Settings.WildPokemonRestrictionMod.CATCH_EM_ALL;
+ boolean typeThemed = settings.getWildPokemonRestrictionMod() == Settings.WildPokemonRestrictionMod.TYPE_THEME_AREAS;
+ boolean usePowerLevels = settings.getWildPokemonRestrictionMod() == Settings.WildPokemonRestrictionMod.SIMILAR_STRENGTH;
+ boolean noLegendaries = settings.isBlockWildLegendaries();
+ int levelModifier = settings.isWildLevelsModified() ? settings.getWildLevelModifier() : 0;
+ boolean allowAltFormes = settings.isAllowWildAltFormes();
+ boolean banIrregularAltFormes = settings.isBanIrregularAltFormes();
+ boolean abilitiesAreRandomized = settings.getAbilitiesMod() == Settings.AbilitiesMod.RANDOMIZE;
+
+ checkPokemonRestrictions();
+ List<Pokemon> banned = this.bannedForWildEncounters();
+ banned.addAll(this.getBannedFormesForPlayerPokemon());
+ if (!abilitiesAreRandomized) {
+ List<Pokemon> abilityDependentFormes = getAbilityDependentFormes();
+ banned.addAll(abilityDependentFormes);
+ }
+ if (banIrregularAltFormes) {
+ banned.addAll(getIrregularFormes());
+ }
+
+ // New: randomize the order encounter sets are randomized in.
+ // Leads to less predictable results for various modifiers.
+ // Need to keep the original ordering around for saving though.
+ List<EncounterSet> scrambledEncounters = new ArrayList<>(currentEncounters);
+ Collections.shuffle(scrambledEncounters, this.random);
+
+ // Assume EITHER catch em all OR type themed for now
+ if (catchEmAll) {
+ List<Pokemon> allPokes;
+ if (allowAltFormes) {
+ allPokes = noLegendaries ? new ArrayList<>(noLegendaryListInclFormes) : new ArrayList<>(
+ mainPokemonListInclFormes);
+ allPokes.removeIf(o -> ((Pokemon) o).actuallyCosmetic);
+ } else {
+ allPokes = noLegendaries ? new ArrayList<>(noLegendaryList) : new ArrayList<>(
+ mainPokemonList);
+ }
+ allPokes.removeAll(banned);
+ for (EncounterSet area : scrambledEncounters) {
+ // Poke-set
+ Set<Pokemon> inArea = pokemonInArea(area);
+ // Build area map using catch em all
+ Map<Pokemon, Pokemon> areaMap = new TreeMap<>();
+ List<Pokemon> pickablePokemon = allPokes;
+ if (area.bannedPokemon.size() > 0) {
+ pickablePokemon = new ArrayList<>(allPokes);
+ pickablePokemon.removeAll(area.bannedPokemon);
+ }
+ for (Pokemon areaPk : inArea) {
+ if (pickablePokemon.size() == 0) {
+ // No more pickable pokes left, take a random one
+ List<Pokemon> tempPickable;
+ if (allowAltFormes) {
+ tempPickable = noLegendaries ? new ArrayList<>(noLegendaryListInclFormes) : new ArrayList<>(
+ mainPokemonListInclFormes);
+ tempPickable.removeIf(o -> ((Pokemon) o).actuallyCosmetic);
+ } else {
+ tempPickable = noLegendaries ? new ArrayList<>(noLegendaryList) : new ArrayList<>(
+ mainPokemonList);
+ }
+ tempPickable.removeAll(banned);
+ tempPickable.removeAll(area.bannedPokemon);
+ if (tempPickable.size() == 0) {
+ throw new RandomizationException("ERROR: Couldn't replace a wild Pokemon!");
+ }
+ int picked = this.random.nextInt(tempPickable.size());
+ Pokemon pickedMN = tempPickable.get(picked);
+ areaMap.put(areaPk, pickedMN);
+ } else {
+ int picked = this.random.nextInt(allPokes.size());
+ Pokemon pickedMN = allPokes.get(picked);
+ areaMap.put(areaPk, pickedMN);
+ pickablePokemon.remove(pickedMN);
+ if (allPokes != pickablePokemon) {
+ allPokes.remove(pickedMN);
+ }
+ if (allPokes.size() == 0) {
+ // Start again
+ if (allowAltFormes) {
+ allPokes.addAll(noLegendaries ? noLegendaryListInclFormes : mainPokemonListInclFormes);
+ allPokes.removeIf(o -> ((Pokemon) o).actuallyCosmetic);
+ } else {
+ allPokes.addAll(noLegendaries ? noLegendaryList : mainPokemonList);
+ }
+ allPokes.removeAll(banned);
+ if (pickablePokemon != allPokes) {
+ pickablePokemon.addAll(allPokes);
+ pickablePokemon.removeAll(area.bannedPokemon);
+ }
+ }
+ }
+ }
+ for (Encounter enc : area.encounters) {
+ // Apply the map
+ enc.pokemon = areaMap.get(enc.pokemon);
+ setFormeForEncounter(enc, enc.pokemon);
+ }
+ }
+ } else if (typeThemed) {
+ Map<Type, List<Pokemon>> cachedPokeLists = new TreeMap<>();
+ for (EncounterSet area : scrambledEncounters) {
+ // Poke-set
+ Set<Pokemon> inArea = pokemonInArea(area);
+ List<Pokemon> possiblePokemon = null;
+ int iterLoops = 0;
+ while (possiblePokemon == null && iterLoops < 10000) {
+ Type areaTheme = randomType();
+ if (!cachedPokeLists.containsKey(areaTheme)) {
+ List<Pokemon> pType = allowAltFormes ? pokemonOfTypeInclFormes(areaTheme, noLegendaries) :
+ pokemonOfType(areaTheme, noLegendaries);
+ pType.removeAll(banned);
+ cachedPokeLists.put(areaTheme, pType);
+ }
+ possiblePokemon = new ArrayList<>(cachedPokeLists.get(areaTheme));
+ if (area.bannedPokemon.size() > 0) {
+ possiblePokemon.removeAll(area.bannedPokemon);
+ }
+ if (possiblePokemon.size() < inArea.size()) {
+ // Can't use this type for this area
+ possiblePokemon = null;
+ }
+ iterLoops++;
+ }
+ if (possiblePokemon == null) {
+ throw new RandomizationException("Could not randomize an area in a reasonable amount of attempts.");
+ }
+
+ // Build area map using type theme.
+ Map<Pokemon, Pokemon> areaMap = new TreeMap<>();
+ for (Pokemon areaPk : inArea) {
+ int picked = this.random.nextInt(possiblePokemon.size());
+ Pokemon pickedMN = possiblePokemon.get(picked);
+ while (pickedMN.actuallyCosmetic) {
+ picked = this.random.nextInt(possiblePokemon.size());
+ pickedMN = possiblePokemon.get(picked);
+ }
+ areaMap.put(areaPk, pickedMN);
+ possiblePokemon.remove(picked);
+ }
+ for (Encounter enc : area.encounters) {
+ // Apply the map
+ enc.pokemon = areaMap.get(enc.pokemon);
+ setFormeForEncounter(enc, enc.pokemon);
+ }
+ }
+ } else if (usePowerLevels) {
+ List<Pokemon> allowedPokes;
+ if (allowAltFormes) {
+ allowedPokes = noLegendaries ? new ArrayList<>(noLegendaryListInclFormes)
+ : new ArrayList<>(mainPokemonListInclFormes);
+ } else {
+ allowedPokes = noLegendaries ? new ArrayList<>(noLegendaryList)
+ : new ArrayList<>(mainPokemonList);
+ }
+ allowedPokes.removeAll(banned);
+ for (EncounterSet area : scrambledEncounters) {
+ // Poke-set
+ Set<Pokemon> inArea = pokemonInArea(area);
+ // Build area map using randoms
+ Map<Pokemon, Pokemon> areaMap = new TreeMap<>();
+ List<Pokemon> usedPks = new ArrayList<>();
+ List<Pokemon> localAllowed = allowedPokes;
+ if (area.bannedPokemon.size() > 0) {
+ localAllowed = new ArrayList<>(allowedPokes);
+ localAllowed.removeAll(area.bannedPokemon);
+ }
+ for (Pokemon areaPk : inArea) {
+ Pokemon picked = pickWildPowerLvlReplacement(localAllowed, areaPk, false, usedPks, 100);
+ while (picked.actuallyCosmetic) {
+ picked = pickWildPowerLvlReplacement(localAllowed, areaPk, false, usedPks, 100);
+ }
+ areaMap.put(areaPk, picked);
+ usedPks.add(picked);
+ }
+ for (Encounter enc : area.encounters) {
+ // Apply the map
+ enc.pokemon = areaMap.get(enc.pokemon);
+ setFormeForEncounter(enc, enc.pokemon);
+ }
+ }
+ } else {
+ // Entirely random
+ for (EncounterSet area : scrambledEncounters) {
+ // Poke-set
+ Set<Pokemon> inArea = pokemonInArea(area);
+ // Build area map using randoms
+ Map<Pokemon, Pokemon> areaMap = new TreeMap<>();
+ for (Pokemon areaPk : inArea) {
+ Pokemon picked = pickEntirelyRandomPokemon(allowAltFormes, noLegendaries, area, banned);
+ while (areaMap.containsValue(picked)) {
+ picked = pickEntirelyRandomPokemon(allowAltFormes, noLegendaries, area, banned);
+ }
+ areaMap.put(areaPk, picked);
+ }
+ for (Encounter enc : area.encounters) {
+ // Apply the map
+ enc.pokemon = areaMap.get(enc.pokemon);
+ setFormeForEncounter(enc, enc.pokemon);
+ }
+ }
+ }
+
+ if (levelModifier != 0) {
+ for (EncounterSet area : currentEncounters) {
+ for (Encounter enc : area.encounters) {
+ enc.level = Math.min(100, (int) Math.round(enc.level * (1 + levelModifier / 100.0)));
+ enc.maxLevel = Math.min(100, (int) Math.round(enc.maxLevel * (1 + levelModifier / 100.0)));
+ }
+ }
+ }
+ }
+
+ @Override
+ public void game1to1Encounters(Settings settings) {
+ boolean useTimeOfDay = settings.isUseTimeBasedEncounters();
+ boolean usePowerLevels = settings.getWildPokemonRestrictionMod() == Settings.WildPokemonRestrictionMod.SIMILAR_STRENGTH;
+ boolean noLegendaries = settings.isBlockWildLegendaries();
+ int levelModifier = settings.isWildLevelsModified() ? settings.getWildLevelModifier() : 0;
+ boolean allowAltFormes = settings.isAllowWildAltFormes();
+ boolean banIrregularAltFormes = settings.isBanIrregularAltFormes();
+ boolean abilitiesAreRandomized = settings.getAbilitiesMod() == Settings.AbilitiesMod.RANDOMIZE;
+
+ checkPokemonRestrictions();
+ // Build the full 1-to-1 map
+ Map<Pokemon, Pokemon> translateMap = new TreeMap<>();
+ List<Pokemon> remainingLeft = allPokemonInclFormesWithoutNull();
+ remainingLeft.removeIf(o -> ((Pokemon) o).actuallyCosmetic);
+ List<Pokemon> remainingRight;
+ if (allowAltFormes) {
+ remainingRight = noLegendaries ? new ArrayList<>(noLegendaryListInclFormes)
+ : new ArrayList<>(mainPokemonListInclFormes);
+ remainingRight.removeIf(o -> ((Pokemon) o).actuallyCosmetic);
+ } else {
+ remainingRight = noLegendaries ? new ArrayList<>(noLegendaryList)
+ : new ArrayList<>(mainPokemonList);
+ }
+ List<Pokemon> banned = this.bannedForWildEncounters();
+ banned.addAll(this.getBannedFormesForPlayerPokemon());
+ if (!abilitiesAreRandomized) {
+ List<Pokemon> abilityDependentFormes = getAbilityDependentFormes();
+ banned.addAll(abilityDependentFormes);
+ }
+ if (banIrregularAltFormes) {
+ banned.addAll(getIrregularFormes());
+ }
+ // Banned pokemon should be mapped to themselves
+ for (Pokemon bannedPK : banned) {
+ translateMap.put(bannedPK, bannedPK);
+ remainingLeft.remove(bannedPK);
+ remainingRight.remove(bannedPK);
+ }
+ while (!remainingLeft.isEmpty()) {
+ if (usePowerLevels) {
+ int pickedLeft = this.random.nextInt(remainingLeft.size());
+ Pokemon pickedLeftP = remainingLeft.remove(pickedLeft);
+ Pokemon pickedRightP;
+ if (remainingRight.size() == 1) {
+ // pick this (it may or may not be the same poke)
+ pickedRightP = remainingRight.get(0);
+ } else {
+ // pick on power level with the current one blocked
+ pickedRightP = pickWildPowerLvlReplacement(remainingRight, pickedLeftP, true, null, 100);
+ }
+ remainingRight.remove(pickedRightP);
+ translateMap.put(pickedLeftP, pickedRightP);
+ } else {
+ int pickedLeft = this.random.nextInt(remainingLeft.size());
+ int pickedRight = this.random.nextInt(remainingRight.size());
+ Pokemon pickedLeftP = remainingLeft.remove(pickedLeft);
+ Pokemon pickedRightP = remainingRight.get(pickedRight);
+ while (pickedLeftP.number == pickedRightP.number && remainingRight.size() != 1) {
+ // Reroll for a different pokemon if at all possible
+ pickedRight = this.random.nextInt(remainingRight.size());
+ pickedRightP = remainingRight.get(pickedRight);
+ }
+ remainingRight.remove(pickedRight);
+ translateMap.put(pickedLeftP, pickedRightP);
+ }
+ if (remainingRight.size() == 0) {
+ // restart
+ if (allowAltFormes) {
+ remainingRight.addAll(noLegendaries ? noLegendaryListInclFormes : mainPokemonListInclFormes);
+ remainingRight.removeIf(o -> ((Pokemon) o).actuallyCosmetic);
+ } else {
+ remainingRight.addAll(noLegendaries ? noLegendaryList : mainPokemonList);
+ }
+ remainingRight.removeAll(banned);
+ }
+ }
+
+ // Map remaining to themselves just in case
+ List<Pokemon> allPokes = allPokemonInclFormesWithoutNull();
+ for (Pokemon poke : allPokes) {
+ if (!translateMap.containsKey(poke)) {
+ translateMap.put(poke, poke);
+ }
+ }
+
+ List<EncounterSet> currentEncounters = this.getEncounters(useTimeOfDay);
+
+ for (EncounterSet area : currentEncounters) {
+ for (Encounter enc : area.encounters) {
+ // Apply the map
+ enc.pokemon = translateMap.get(enc.pokemon);
+ if (area.bannedPokemon.contains(enc.pokemon)) {
+ // Ignore the map and put a random non-banned poke
+ List<Pokemon> tempPickable;
+ if (allowAltFormes) {
+ tempPickable = noLegendaries ? new ArrayList<>(noLegendaryListInclFormes)
+ : new ArrayList<>(mainPokemonListInclFormes);
+ tempPickable.removeIf(o -> ((Pokemon) o).actuallyCosmetic);
+ } else {
+ tempPickable = noLegendaries ? new ArrayList<>(noLegendaryList)
+ : new ArrayList<>(mainPokemonList);
+ }
+ tempPickable.removeAll(banned);
+ tempPickable.removeAll(area.bannedPokemon);
+ if (tempPickable.size() == 0) {
+ throw new RandomizationException("ERROR: Couldn't replace a wild Pokemon!");
+ }
+ if (usePowerLevels) {
+ enc.pokemon = pickWildPowerLvlReplacement(tempPickable, enc.pokemon, false, null, 100);
+ } else {
+ int picked = this.random.nextInt(tempPickable.size());
+ enc.pokemon = tempPickable.get(picked);
+ }
+ }
+ setFormeForEncounter(enc, enc.pokemon);
+ }
+ }
+ if (levelModifier != 0) {
+ for (EncounterSet area : currentEncounters) {
+ for (Encounter enc : area.encounters) {
+ enc.level = Math.min(100, (int) Math.round(enc.level * (1 + levelModifier / 100.0)));
+ enc.maxLevel = Math.min(100, (int) Math.round(enc.maxLevel * (1 + levelModifier / 100.0)));
+ }
+ }
+ }
+
+ setEncounters(useTimeOfDay, currentEncounters);
+
+ }
+
+ @Override
+ public void onlyChangeWildLevels(Settings settings) {
+ int levelModifier = settings.getWildLevelModifier();
+
+ List<EncounterSet> currentEncounters = this.getEncounters(true);
+
+ if (levelModifier != 0) {
+ for (EncounterSet area : currentEncounters) {
+ for (Encounter enc : area.encounters) {
+ enc.level = Math.min(100, (int) Math.round(enc.level * (1 + levelModifier / 100.0)));
+ enc.maxLevel = Math.min(100, (int) Math.round(enc.maxLevel * (1 + levelModifier / 100.0)));
+ }
+ }
+ setEncounters(true, currentEncounters);
+ }
+ }
+
+ private void enhanceRandomEncountersORAS(List<EncounterSet> collapsedEncounters, Settings settings) {
+ boolean catchEmAll = settings.getWildPokemonRestrictionMod() == Settings.WildPokemonRestrictionMod.CATCH_EM_ALL;
+ boolean typeThemed = settings.getWildPokemonRestrictionMod() == Settings.WildPokemonRestrictionMod.TYPE_THEME_AREAS;
+ boolean usePowerLevels = settings.getWildPokemonRestrictionMod() == Settings.WildPokemonRestrictionMod.SIMILAR_STRENGTH;
+ boolean noLegendaries = settings.isBlockWildLegendaries();
+ boolean allowAltFormes = settings.isAllowWildAltFormes();
+ boolean banIrregularAltFormes = settings.isBanIrregularAltFormes();
+ boolean abilitiesAreRandomized = settings.getAbilitiesMod() == Settings.AbilitiesMod.RANDOMIZE;
+
+ List<Pokemon> banned = this.bannedForWildEncounters();
+ if (!abilitiesAreRandomized) {
+ List<Pokemon> abilityDependentFormes = getAbilityDependentFormes();
+ banned.addAll(abilityDependentFormes);
+ }
+ if (banIrregularAltFormes) {
+ banned.addAll(getIrregularFormes());
+ }
+ Map<Integer, List<EncounterSet>> zonesToEncounters = mapZonesToEncounters(collapsedEncounters);
+ Map<Type, List<Pokemon>> cachedPokeLists = new TreeMap<>();
+ for (List<EncounterSet> encountersInZone : zonesToEncounters.values()) {
+ int currentAreaIndex = -1;
+ List<EncounterSet> nonRockSmashAreas = new ArrayList<>();
+ Map<Integer, List<Integer>> areasAndEncountersToRandomize = new TreeMap<>();
+ // Since Rock Smash Pokemon do not show up on DexNav, they can be fully randomized
+ for (EncounterSet area : encountersInZone) {
+ if (area.displayName.contains("Rock Smash")) {
+ // Assume EITHER catch em all OR type themed OR match strength for now
+ if (catchEmAll) {
+ for (Encounter enc : area.encounters) {
+ boolean shouldRandomize = doesAnotherEncounterWithSamePokemonExistInArea(enc, area);
+ if (shouldRandomize) {
+ enc.pokemon = pickEntirelyRandomPokemon(allowAltFormes, noLegendaries, area, banned);
+ setFormeForEncounter(enc, enc.pokemon);
+ }
+ }
+ } else if (typeThemed) {
+ List<Pokemon> possiblePokemon = null;
+ int iterLoops = 0;
+ while (possiblePokemon == null && iterLoops < 10000) {
+ Type areaTheme = randomType();
+ if (!cachedPokeLists.containsKey(areaTheme)) {
+ List<Pokemon> pType = allowAltFormes ? pokemonOfTypeInclFormes(areaTheme, noLegendaries) :
+ pokemonOfType(areaTheme, noLegendaries);
+ pType.removeAll(banned);
+ cachedPokeLists.put(areaTheme, pType);
+ }
+ possiblePokemon = cachedPokeLists.get(areaTheme);
+ if (area.bannedPokemon.size() > 0) {
+ possiblePokemon = new ArrayList<>(possiblePokemon);
+ possiblePokemon.removeAll(area.bannedPokemon);
+ }
+ if (possiblePokemon.size() == 0) {
+ // Can't use this type for this area
+ possiblePokemon = null;
+ }
+ iterLoops++;
+ }
+ if (possiblePokemon == null) {
+ throw new RandomizationException("Could not randomize an area in a reasonable amount of attempts.");
+ }
+ for (Encounter enc : area.encounters) {
+ // Pick a random themed pokemon
+ enc.pokemon = possiblePokemon.get(this.random.nextInt(possiblePokemon.size()));
+ while (enc.pokemon.actuallyCosmetic) {
+ enc.pokemon = possiblePokemon.get(this.random.nextInt(possiblePokemon.size()));
+ }
+ setFormeForEncounter(enc, enc.pokemon);
+ }
+ } else if (usePowerLevels) {
+ List<Pokemon> allowedPokes;
+ if (allowAltFormes) {
+ allowedPokes = noLegendaries ? new ArrayList<>(noLegendaryListInclFormes)
+ : new ArrayList<>(mainPokemonListInclFormes);
+ } else {
+ allowedPokes = noLegendaries ? new ArrayList<>(noLegendaryList)
+ : new ArrayList<>(mainPokemonList);
+ }
+ allowedPokes.removeAll(banned);
+ List<Pokemon> localAllowed = allowedPokes;
+ if (area.bannedPokemon.size() > 0) {
+ localAllowed = new ArrayList<>(allowedPokes);
+ localAllowed.removeAll(area.bannedPokemon);
+ }
+ for (Encounter enc : area.encounters) {
+ enc.pokemon = pickWildPowerLvlReplacement(localAllowed, enc.pokemon, false, null, 100);
+ while (enc.pokemon.actuallyCosmetic) {
+ enc.pokemon = pickWildPowerLvlReplacement(localAllowed, enc.pokemon, false, null, 100);
+ }
+ setFormeForEncounter(enc, enc.pokemon);
+ }
+ } else {
+ // Entirely random
+ for (Encounter enc : area.encounters) {
+ enc.pokemon = pickEntirelyRandomPokemon(allowAltFormes, noLegendaries, area, banned);
+ setFormeForEncounter(enc, enc.pokemon);
+ }
+ }
+ } else {
+ currentAreaIndex++;
+ nonRockSmashAreas.add(area);
+ List<Integer> encounterIndices = new ArrayList<>();
+ for (int i = 0; i < area.encounters.size(); i++) {
+ encounterIndices.add(i);
+ }
+ areasAndEncountersToRandomize.put(currentAreaIndex, encounterIndices);
+ }
+ }
+
+ // Now, randomize non-Rock Smash Pokemon until we hit the threshold for DexNav
+ int crashThreshold = computeDexNavCrashThreshold(encountersInZone);
+ while (crashThreshold < 18 && areasAndEncountersToRandomize.size() > 0) {
+ Set<Integer> areaIndices = areasAndEncountersToRandomize.keySet();
+ int areaIndex = areaIndices.stream().skip(this.random.nextInt(areaIndices.size())).findFirst().orElse(-1);
+ List<Integer> encounterIndices = areasAndEncountersToRandomize.get(areaIndex);
+ int indexInListOfEncounterIndices = this.random.nextInt(encounterIndices.size());
+ int randomEncounterIndex = encounterIndices.get(indexInListOfEncounterIndices);
+ EncounterSet area = nonRockSmashAreas.get(areaIndex);
+ Encounter enc = area.encounters.get(randomEncounterIndex);
+ // Assume EITHER catch em all OR type themed OR match strength for now
+ if (catchEmAll) {
+ boolean shouldRandomize = doesAnotherEncounterWithSamePokemonExistInArea(enc, area);
+ if (shouldRandomize) {
+ enc.pokemon = pickEntirelyRandomPokemon(allowAltFormes, noLegendaries, area, banned);
+ setFormeForEncounter(enc, enc.pokemon);
+ }
+ } else if (typeThemed) {
+ List<Pokemon> possiblePokemon = null;
+ Type areaTheme = getTypeForArea(area);
+ if (!cachedPokeLists.containsKey(areaTheme)) {
+ List<Pokemon> pType = allowAltFormes ? pokemonOfTypeInclFormes(areaTheme, noLegendaries) :
+ pokemonOfType(areaTheme, noLegendaries);
+ pType.removeAll(banned);
+ cachedPokeLists.put(areaTheme, pType);
+ }
+ possiblePokemon = cachedPokeLists.get(areaTheme);
+ if (area.bannedPokemon.size() > 0) {
+ possiblePokemon = new ArrayList<>(possiblePokemon);
+ possiblePokemon.removeAll(area.bannedPokemon);
+ }
+ if (possiblePokemon.size() == 0) {
+ // Can't use this type for this area
+ throw new RandomizationException("Could not find a possible Pokemon of the correct type.");
+ }
+ // Pick a random themed pokemon
+ enc.pokemon = possiblePokemon.get(this.random.nextInt(possiblePokemon.size()));
+ while (enc.pokemon.actuallyCosmetic) {
+ enc.pokemon = possiblePokemon.get(this.random.nextInt(possiblePokemon.size()));
+ }
+ setFormeForEncounter(enc, enc.pokemon);
+ } else if (usePowerLevels) {
+ List<Pokemon> allowedPokes;
+ if (allowAltFormes) {
+ allowedPokes = noLegendaries ? new ArrayList<>(noLegendaryListInclFormes)
+ : new ArrayList<>(mainPokemonListInclFormes);
+ } else {
+ allowedPokes = noLegendaries ? new ArrayList<>(noLegendaryList)
+ : new ArrayList<>(mainPokemonList);
+ }
+ allowedPokes.removeAll(banned);
+ List<Pokemon> localAllowed = allowedPokes;
+ if (area.bannedPokemon.size() > 0) {
+ localAllowed = new ArrayList<>(allowedPokes);
+ localAllowed.removeAll(area.bannedPokemon);
+ }
+ enc.pokemon = pickWildPowerLvlReplacement(localAllowed, enc.pokemon, false, null, 100);
+ while (enc.pokemon.actuallyCosmetic) {
+ enc.pokemon = pickWildPowerLvlReplacement(localAllowed, enc.pokemon, false, null, 100);
+ }
+ setFormeForEncounter(enc, enc.pokemon);
+ } else {
+ // Entirely random
+ enc.pokemon = pickEntirelyRandomPokemon(allowAltFormes, noLegendaries, area, banned);
+ setFormeForEncounter(enc, enc.pokemon);
+ }
+ crashThreshold = computeDexNavCrashThreshold(encountersInZone);
+ encounterIndices.remove(indexInListOfEncounterIndices);
+ if (encounterIndices.size() == 0) {
+ areasAndEncountersToRandomize.remove(areaIndex);
+ }
+ }
+ }
+ }
+
+ private Type getTypeForArea(EncounterSet area) {
+ Pokemon firstPokemon = area.encounters.get(0).pokemon;
+ if (area.encounters.get(0).formeNumber != 0) {
+ firstPokemon = getAltFormeOfPokemon(firstPokemon, area.encounters.get(0).formeNumber);
+ }
+ Type primaryType = firstPokemon.primaryType;
+ int primaryCount = 1;
+ Type secondaryType = null;
+ int secondaryCount = 0;
+ if (firstPokemon.secondaryType != null) {
+ secondaryType = firstPokemon.secondaryType;
+ secondaryCount = 1;
+ }
+ for (int i = 1; i < area.encounters.size(); i++) {
+ Pokemon pokemon = area.encounters.get(i).pokemon;
+ if (area.encounters.get(i).formeNumber != 0) {
+ pokemon = getAltFormeOfPokemon(pokemon, area.encounters.get(i).formeNumber);
+ }
+ if (pokemon.primaryType == primaryType || pokemon.secondaryType == primaryType) {
+ primaryCount++;
+ }
+ if (pokemon.primaryType == secondaryType || pokemon.secondaryType == secondaryType) {
+ secondaryCount++;
+ }
+ }
+ return primaryCount > secondaryCount ? primaryType : secondaryType;
+ }
+
+ private boolean doesAnotherEncounterWithSamePokemonExistInArea(Encounter enc, EncounterSet area) {
+ for (Encounter encounterToCheck : area.encounters) {
+ if (enc != encounterToCheck && enc.pokemon == encounterToCheck.pokemon) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ private List<EncounterSet> collapseAreasORAS(List<EncounterSet> currentEncounters) {
+ List<EncounterSet> output = new ArrayList<>();
+ Map<Integer, List<EncounterSet>> zonesToEncounters = mapZonesToEncounters(currentEncounters);
+ for (Integer zone : zonesToEncounters.keySet()) {
+ List<EncounterSet> encountersInZone = zonesToEncounters.get(zone);
+ int crashThreshold = computeDexNavCrashThreshold(encountersInZone);
+ if (crashThreshold <= 18) {
+ output.addAll(encountersInZone);
+ continue;
+ }
+
+ // Naive Area 1-to-1 randomization will crash the game, so let's start collapsing areas to prevent this.
+ // Start with combining all the fishing rod encounters, since it's a little less noticeable when they've
+ // been collapsed.
+ List<EncounterSet> collapsedEncounters = new ArrayList<>(encountersInZone);
+ EncounterSet rodGroup = new EncounterSet();
+ rodGroup.offset = zone;
+ rodGroup.displayName = "Rod Group";
+ for (EncounterSet area : encountersInZone) {
+ if (area.displayName.contains("Old Rod") || area.displayName.contains("Good Rod") || area.displayName.contains("Super Rod")) {
+ collapsedEncounters.remove(area);
+ rodGroup.encounters.addAll(area.encounters);
+ }
+ }
+ if (rodGroup.encounters.size() > 0) {
+ collapsedEncounters.add(rodGroup);
+ }
+ crashThreshold = computeDexNavCrashThreshold(collapsedEncounters);
+ if (crashThreshold <= 18) {
+ output.addAll(collapsedEncounters);
+ continue;
+ }
+
+ // Even after combining all the fishing rod encounters, we're still not below the threshold to prevent
+ // DexNav from crashing the game. Combine all the grass encounters now to drop us below the threshold;
+ // we've combined everything that DexNav normally combines, so at this point, we're *guaranteed* not
+ // to crash the game.
+ EncounterSet grassGroup = new EncounterSet();
+ grassGroup.offset = zone;
+ grassGroup.displayName = "Grass Group";
+ for (EncounterSet area : encountersInZone) {
+ if (area.displayName.contains("Grass/Cave") || area.displayName.contains("Long Grass") || area.displayName.contains("Horde")) {
+ collapsedEncounters.remove(area);
+ grassGroup.encounters.addAll(area.encounters);
+ }
+ }
+ if (grassGroup.encounters.size() > 0) {
+ collapsedEncounters.add(grassGroup);
+ }
+
+ output.addAll(collapsedEncounters);
+ }
+ return output;
+ }
+
+ private int computeDexNavCrashThreshold(List<EncounterSet> encountersInZone) {
+ int crashThreshold = 0;
+ for (EncounterSet area : encountersInZone) {
+ if (area.displayName.contains("Rock Smash")) {
+ continue; // Rock Smash Pokemon don't display on DexNav
+ }
+ Set<Pokemon> uniquePokemonInArea = new HashSet<>();
+ for (Encounter enc : area.encounters) {
+ if (enc.pokemon.baseForme != null) { // DexNav treats different forms as one Pokemon
+ uniquePokemonInArea.add(enc.pokemon.baseForme);
+ } else {
+ uniquePokemonInArea.add(enc.pokemon);
+ }
+ }
+ crashThreshold += uniquePokemonInArea.size();
+ }
+ return crashThreshold;
+ }
+
+ private void setEvoChainAsIllegal(Pokemon newPK, List<Pokemon> illegalList, boolean willForceEvolve) {
+ // set pre-evos as illegal
+ setIllegalPreEvos(newPK, illegalList);
+
+ // if the placed Pokemon will be forced fully evolved, set its evolutions as illegal
+ if (willForceEvolve) {
+ setIllegalEvos(newPK, illegalList);
+ }
+ }
+
+ private void setIllegalPreEvos(Pokemon pk, List<Pokemon> illegalList) {
+ for (Evolution evo: pk.evolutionsTo) {
+ pk = evo.from;
+ illegalList.add(pk);
+ setIllegalPreEvos(pk, illegalList);
+ }
+ }
+
+ private void setIllegalEvos(Pokemon pk, List<Pokemon> illegalList) {
+ for (Evolution evo: pk.evolutionsFrom) {
+ pk = evo.to;
+ illegalList.add(pk);
+ setIllegalEvos(pk, illegalList);
+ }
+ }
+
+ private List<Pokemon> getFinalEvos(Pokemon pk) {
+ List<Pokemon> finalEvos = new ArrayList<>();
+ traverseEvolutions(pk, finalEvos);
+ return finalEvos;
+ }
+
+ private void traverseEvolutions(Pokemon pk, List<Pokemon> finalEvos) {
+ if (!pk.evolutionsFrom.isEmpty()) {
+ for (Evolution evo: pk.evolutionsFrom) {
+ pk = evo.to;
+ traverseEvolutions(pk, finalEvos);
+ }
+ } else {
+ finalEvos.add(pk);
+ }
+ }
+
+ private void setFormeForTrainerPokemon(TrainerPokemon tp, Pokemon pk) {
+ boolean checkCosmetics = true;
+ tp.formeSuffix = "";
+ tp.forme = 0;
+ if (pk.formeNumber > 0) {
+ tp.forme = pk.formeNumber;
+ tp.formeSuffix = pk.formeSuffix;
+ tp.pokemon = pk.baseForme;
+ checkCosmetics = false;
+ }
+ if (checkCosmetics && tp.pokemon.cosmeticForms > 0) {
+ tp.forme = tp.pokemon.getCosmeticFormNumber(this.random.nextInt(tp.pokemon.cosmeticForms));
+ } else if (!checkCosmetics && pk.cosmeticForms > 0) {
+ tp.forme += pk.getCosmeticFormNumber(this.random.nextInt(pk.cosmeticForms));
+ }
+ }
+
+ private void applyLevelModifierToTrainerPokemon(Trainer trainer, int levelModifier) {
+ if (levelModifier != 0) {
+ for (TrainerPokemon tp : trainer.pokemon) {
+ tp.level = Math.min(100, (int) Math.round(tp.level * (1 + levelModifier / 100.0)));
+ }
+ }
+ }
+
+ @Override
+ public void randomizeTrainerPokes(Settings settings) {
+ boolean usePowerLevels = settings.isTrainersUsePokemonOfSimilarStrength();
+ boolean weightByFrequency = settings.isTrainersMatchTypingDistribution();
+ boolean noLegendaries = settings.isTrainersBlockLegendaries();
+ boolean noEarlyWonderGuard = settings.isTrainersBlockEarlyWonderGuard();
+ int levelModifier = settings.isTrainersLevelModified() ? settings.getTrainersLevelModifier() : 0;
+ boolean isTypeThemed = settings.getTrainersMod() == Settings.TrainersMod.TYPE_THEMED;
+ boolean isTypeThemedEliteFourGymOnly = settings.getTrainersMod() == Settings.TrainersMod.TYPE_THEMED_ELITE4_GYMS;
+ boolean distributionSetting = settings.getTrainersMod() == Settings.TrainersMod.DISTRIBUTED;
+ boolean mainPlaythroughSetting = settings.getTrainersMod() == Settings.TrainersMod.MAINPLAYTHROUGH;
+ boolean includeFormes = settings.isAllowTrainerAlternateFormes();
+ boolean banIrregularAltFormes = settings.isBanIrregularAltFormes();
+ boolean swapMegaEvos = settings.isSwapTrainerMegaEvos();
+ boolean shinyChance = settings.isShinyChance();
+ boolean abilitiesAreRandomized = settings.getAbilitiesMod() == Settings.AbilitiesMod.RANDOMIZE;
+ int eliteFourUniquePokemonNumber = settings.getEliteFourUniquePokemonNumber();
+ boolean forceFullyEvolved = settings.isTrainersForceFullyEvolved();
+ int forceFullyEvolvedLevel = settings.getTrainersForceFullyEvolvedLevel();
+ boolean forceChallengeMode = (settings.getCurrentMiscTweaks() & MiscTweak.FORCE_CHALLENGE_MODE.getValue()) > 0;
+ boolean rivalCarriesStarter = settings.isRivalCarriesStarterThroughout();
+
+ checkPokemonRestrictions();
+
+ // Set up Pokemon pool
+ cachedReplacementLists = new TreeMap<>();
+ cachedAllList = noLegendaries ? new ArrayList<>(noLegendaryList) : new ArrayList<>(
+ mainPokemonList);
+ if (includeFormes) {
+ if (noLegendaries) {
+ cachedAllList.addAll(noLegendaryAltsList);
+ } else {
+ cachedAllList.addAll(altFormesList);
+ }
+ }
+ cachedAllList =
+ cachedAllList
+ .stream()
+ .filter(pk -> !pk.actuallyCosmetic)
+ .collect(Collectors.toList());
+
+ List<Pokemon> banned = this.getBannedFormesForTrainerPokemon();
+ if (!abilitiesAreRandomized) {
+ List<Pokemon> abilityDependentFormes = getAbilityDependentFormes();
+ banned.addAll(abilityDependentFormes);
+ }
+ if (banIrregularAltFormes) {
+ banned.addAll(getIrregularFormes());
+ }
+ cachedAllList.removeAll(banned);
+
+ List<Trainer> currentTrainers = this.getTrainers();
+
+ // Type Themed related
+ Map<Trainer, Type> trainerTypes = new TreeMap<>();
+ Set<Type> usedUberTypes = new TreeSet<>();
+ if (isTypeThemed || isTypeThemedEliteFourGymOnly) {
+ typeWeightings = new TreeMap<>();
+ totalTypeWeighting = 0;
+ // Construct groupings for types
+ // Anything starting with GYM or ELITE or CHAMPION is a group
+ Map<String, List<Trainer>> groups = new TreeMap<>();
+ for (Trainer t : currentTrainers) {
+ if (t.tag != null && t.tag.equals("IRIVAL")) {
+ // This is the first rival in Yellow. His Pokemon is used to determine the non-player
+ // starter, so we can't change it here. Just skip it.
+ continue;
+ }
+ String group = t.tag == null ? "" : t.tag;
+ if (group.contains("-")) {
+ group = group.substring(0, group.indexOf('-'));
+ }
+ if (group.startsWith("GYM") || group.startsWith("ELITE") ||
+ ((group.startsWith("CHAMPION") || group.startsWith("THEMED")) && !isTypeThemedEliteFourGymOnly)) {
+ // Yep this is a group
+ if (!groups.containsKey(group)) {
+ groups.put(group, new ArrayList<>());
+ }
+ groups.get(group).add(t);
+ } else if (group.startsWith("GIO")) {
+ // Giovanni has same grouping as his gym, gym 8
+ if (!groups.containsKey("GYM8")) {
+ groups.put("GYM8", new ArrayList<>());
+ }
+ groups.get("GYM8").add(t);
+ }
+ }
+
+ // Give a type to each group
+ // Gym & elite types have to be unique
+ // So do uber types, including the type we pick for champion
+ Set<Type> usedGymTypes = new TreeSet<>();
+ Set<Type> usedEliteTypes = new TreeSet<>();
+ for (String group : groups.keySet()) {
+ List<Trainer> trainersInGroup = groups.get(group);
+ // Shuffle ordering within group to promote randomness
+ Collections.shuffle(trainersInGroup, random);
+ Type typeForGroup = pickType(weightByFrequency, noLegendaries, includeFormes);
+ if (group.startsWith("GYM")) {
+ while (usedGymTypes.contains(typeForGroup)) {
+ typeForGroup = pickType(weightByFrequency, noLegendaries, includeFormes);
+ }
+ usedGymTypes.add(typeForGroup);
+ }
+ if (group.startsWith("ELITE")) {
+ while (usedEliteTypes.contains(typeForGroup)) {
+ typeForGroup = pickType(weightByFrequency, noLegendaries, includeFormes);
+ }
+ usedEliteTypes.add(typeForGroup);
+ }
+ if (group.equals("CHAMPION")) {
+ usedUberTypes.add(typeForGroup);
+ }
+
+ for (Trainer t : trainersInGroup) {
+ trainerTypes.put(t, typeForGroup);
+ }
+ }
+ }
+
+ // Randomize the order trainers are randomized in.
+ // Leads to less predictable results for various modifiers.
+ // Need to keep the original ordering around for saving though.
+ List<Trainer> scrambledTrainers = new ArrayList<>(currentTrainers);
+ Collections.shuffle(scrambledTrainers, this.random);
+
+ // Elite Four Unique Pokemon related
+ boolean eliteFourUniquePokemon = eliteFourUniquePokemonNumber > 0;
+ List<Pokemon> illegalIfEvolvedList = new ArrayList<>();
+ List<Pokemon> bannedFromUniqueList = new ArrayList<>();
+ boolean illegalEvoChains = false;
+ List<Integer> eliteFourIndices = getEliteFourTrainers(forceChallengeMode);
+ if (eliteFourUniquePokemon) {
+ // Sort Elite Four Trainers to the start of the list
+ scrambledTrainers.sort((t1, t2) ->
+ Boolean.compare(eliteFourIndices.contains(currentTrainers.indexOf(t2)+1),eliteFourIndices.contains(currentTrainers.indexOf(t1)+1)));
+ illegalEvoChains = forceFullyEvolved;
+ if (rivalCarriesStarter) {
+ List<Pokemon> starterList = getStarters().subList(0,3);
+ for (Pokemon starter: starterList) {
+ // If rival/friend carries starter, the starters cannot be set as unique
+ bannedFromUniqueList.add(starter);
+ setEvoChainAsIllegal(starter, bannedFromUniqueList, true);
+
+ // If the final boss is a rival/friend, the fully evolved starters will be unique
+ if (hasRivalFinalBattle()) {
+ cachedAllList.removeAll(getFinalEvos(starter));
+ if (illegalEvoChains) {
+ illegalIfEvolvedList.add(starter);
+ setEvoChainAsIllegal(starter, illegalIfEvolvedList, true);
+ }
+ }
+ }
+ }
+ }
+
+ List<Integer> mainPlaythroughTrainers = getMainPlaythroughTrainers();
+
+ // Randomize Trainer Pokemon
+ // The result after this is done will not be final if "Force Fully Evolved" or "Rival Carries Starter"
+ // are used, as they are applied later
+ for (Trainer t : scrambledTrainers) {
+ applyLevelModifierToTrainerPokemon(t, levelModifier);
+ if (t.tag != null && t.tag.equals("IRIVAL")) {
+ // This is the first rival in Yellow. His Pokemon is used to determine the non-player
+ // starter, so we can't change it here. Just skip it.
+ continue;
+ }
+
+ // If type themed, give a type to each unassigned trainer
+ Type typeForTrainer = trainerTypes.get(t);
+ if (typeForTrainer == null && isTypeThemed) {
+ typeForTrainer = pickType(weightByFrequency, noLegendaries, includeFormes);
+ // Ubers: can't have the same type as each other
+ if (t.tag != null && t.tag.equals("UBER")) {
+ while (usedUberTypes.contains(typeForTrainer)) {
+ typeForTrainer = pickType(weightByFrequency, noLegendaries, includeFormes);
+ }
+ usedUberTypes.add(typeForTrainer);
+ }
+ }
+
+ int trainerIndex = currentTrainers.indexOf(t) + 1;
+
+ List<TrainerPokemon> trainerPokemonList = new ArrayList<>(t.pokemon);
+
+ // Elite Four Unique Pokemon related
+ boolean eliteFourTrackPokemon = false;
+ boolean eliteFourRival = false;
+ if (eliteFourUniquePokemon && eliteFourIndices.contains(trainerIndex)) {
+ eliteFourTrackPokemon = true;
+
+ // Sort Pokemon list back to front, and then put highest level Pokemon first
+ // (Only while randomizing, does not affect order in game)
+ Collections.reverse(trainerPokemonList);
+ trainerPokemonList.sort((tp1, tp2) -> Integer.compare(tp2.level, tp1.level));
+ if (rivalCarriesStarter && (t.tag.contains("RIVAL") || t.tag.contains("FRIEND"))) {
+ eliteFourRival = true;
+ }
+ }
+
+ for (TrainerPokemon tp : trainerPokemonList) {
+ boolean swapThisMegaEvo = swapMegaEvos && tp.canMegaEvolve();
+ boolean wgAllowed = (!noEarlyWonderGuard) || tp.level >= 20;
+ boolean eliteFourSetUniquePokemon =
+ eliteFourTrackPokemon && eliteFourUniquePokemonNumber > trainerPokemonList.indexOf(tp);
+ boolean willForceEvolve = forceFullyEvolved && tp.level >= forceFullyEvolvedLevel;
+
+ Pokemon oldPK = tp.pokemon;
+ if (tp.forme > 0) {
+ oldPK = getAltFormeOfPokemon(oldPK, tp.forme);
+ }
+
+ bannedList = new ArrayList<>();
+ bannedList.addAll(usedAsUniqueList);
+ if (illegalEvoChains && willForceEvolve) {
+ bannedList.addAll(illegalIfEvolvedList);
+ }
+ if (eliteFourSetUniquePokemon) {
+ bannedList.addAll(bannedFromUniqueList);
+ }
+
+ Pokemon newPK = pickTrainerPokeReplacement(
+ oldPK,
+ usePowerLevels,
+ typeForTrainer,
+ noLegendaries,
+ wgAllowed,
+ distributionSetting || (mainPlaythroughSetting && mainPlaythroughTrainers.contains(t.offset)),
+ swapThisMegaEvo,
+ abilitiesAreRandomized,
+ includeFormes,
+ banIrregularAltFormes
+ );
+
+ // Chosen Pokemon is locked in past here
+ if (distributionSetting || (mainPlaythroughSetting && mainPlaythroughTrainers.contains(t.offset))) {
+ setPlacementHistory(newPK);
+ }
+ tp.absolutePokeNumber = newPK.number;
+ tp.pokemon = newPK;
+ setFormeForTrainerPokemon(tp, newPK);
+ tp.abilitySlot = getRandomAbilitySlot(newPK);
+ tp.resetMoves = true;
+
+ if (!eliteFourRival) {
+ if (eliteFourSetUniquePokemon) {
+ List<Pokemon> actualPKList;
+ if (willForceEvolve) {
+ actualPKList = getFinalEvos(newPK);
+ } else {
+ actualPKList = new ArrayList<>();
+ actualPKList.add(newPK);
+ }
+ // If the unique Pokemon will evolve, we have to set all its potential evolutions as unique
+ for (Pokemon actualPK: actualPKList) {
+ usedAsUniqueList.add(actualPK);
+ if (illegalEvoChains) {
+ setEvoChainAsIllegal(actualPK, illegalIfEvolvedList, willForceEvolve);
+ }
+ }
+ }
+ if (eliteFourTrackPokemon) {
+ bannedFromUniqueList.add(newPK);
+ if (illegalEvoChains) {
+ setEvoChainAsIllegal(newPK, bannedFromUniqueList, willForceEvolve);
+ }
+ }
+ } else {
+ // If the champion is a rival, the first Pokemon will be skipped - it's already
+ // set as unique since it's a starter
+ eliteFourRival = false;
+ }
+
+ if (swapThisMegaEvo) {
+ tp.heldItem = newPK
+ .megaEvolutionsFrom
+ .get(this.random.nextInt(newPK.megaEvolutionsFrom.size()))
+ .argument;
+ }
+
+ if (shinyChance) {
+ if (this.random.nextInt(256) == 0) {
+ tp.IVs |= (1 << 30);
+ }
+ }
+ }
+ }
+
+ // Save it all up
+ this.setTrainers(currentTrainers, false);
+ }
+
+ @Override
+ public void randomizeTrainerHeldItems(Settings settings) {
+ boolean giveToBossPokemon = settings.isRandomizeHeldItemsForBossTrainerPokemon();
+ boolean giveToImportantPokemon = settings.isRandomizeHeldItemsForImportantTrainerPokemon();
+ boolean giveToRegularPokemon = settings.isRandomizeHeldItemsForRegularTrainerPokemon();
+ boolean highestLevelOnly = settings.isHighestLevelGetsItemsForTrainers();
+
+ List<Move> moves = this.getMoves();
+ Map<Integer, List<MoveLearnt>> movesets = this.getMovesLearnt();
+ List<Trainer> currentTrainers = this.getTrainers();
+ for (Trainer t : currentTrainers) {
+ if (trainerShouldNotGetBuffs(t)) {
+ continue;
+ }
+ if (!giveToRegularPokemon && (!t.isImportant() && !t.isBoss())) {
+ continue;
+ }
+ if (!giveToImportantPokemon && t.isImportant()) {
+ continue;
+ }
+ if (!giveToBossPokemon && t.isBoss()) {
+ continue;
+ }
+ t.setPokemonHaveItems(true);
+ if (highestLevelOnly) {
+ int maxLevel = -1;
+ TrainerPokemon highestLevelPoke = null;
+ for (TrainerPokemon tp : t.pokemon) {
+ if (tp.level > maxLevel) {
+ highestLevelPoke = tp;
+ maxLevel = tp.level;
+ }
+ }
+ if (highestLevelPoke == null) {
+ continue; // should never happen - trainer had zero pokes
+ }
+ randomizeHeldItem(highestLevelPoke, settings, moves, movesets);
+ } else {
+ for (TrainerPokemon tp : t.pokemon) {
+ randomizeHeldItem(tp, settings, moves, movesets);
+ }
+ }
+ }
+ this.setTrainers(currentTrainers, false);
+ }
+
+ private void randomizeHeldItem(TrainerPokemon tp, Settings settings, List<Move> moves, Map<Integer, List<MoveLearnt>> movesets) {
+ boolean sensibleItemsOnly = settings.isSensibleItemsOnlyForTrainers();
+ boolean consumableItemsOnly = settings.isConsumableItemsOnlyForTrainers();
+ boolean swapMegaEvolutions = settings.isSwapTrainerMegaEvos();
+ if (tp.hasZCrystal) {
+ return; // Don't overwrite existing Z Crystals.
+ }
+ if (tp.hasMegaStone && swapMegaEvolutions) {
+ return; // Don't overwrite mega stones if another setting handled that.
+ }
+ List<Integer> toChooseFrom;
+ if (sensibleItemsOnly) {
+ toChooseFrom = getSensibleHeldItemsFor(tp, consumableItemsOnly, moves, movesets);
+ } else if (consumableItemsOnly) {
+ toChooseFrom = getAllConsumableHeldItems();
+ } else {
+ toChooseFrom = getAllHeldItems();
+ }
+ tp.heldItem = toChooseFrom.get(random.nextInt(toChooseFrom.size()));
+ }
+
+ @Override
+ public void rivalCarriesStarter() {
+ checkPokemonRestrictions();
+ List<Trainer> currentTrainers = this.getTrainers();
+ rivalCarriesStarterUpdate(currentTrainers, "RIVAL", isORAS ? 0 : 1);
+ rivalCarriesStarterUpdate(currentTrainers, "FRIEND", 2);
+ this.setTrainers(currentTrainers, false);
+ }
+
+ @Override
+ public boolean hasRivalFinalBattle() {
+ return false;
+ }
+
+ @Override
+ public void forceFullyEvolvedTrainerPokes(Settings settings) {
+ int minLevel = settings.getTrainersForceFullyEvolvedLevel();
+
+ checkPokemonRestrictions();
+ List<Trainer> currentTrainers = this.getTrainers();
+ for (Trainer t : currentTrainers) {
+ for (TrainerPokemon tp : t.pokemon) {
+ if (tp.level >= minLevel) {
+ Pokemon newPokemon = fullyEvolve(tp.pokemon);
+ if (newPokemon != tp.pokemon) {
+ tp.pokemon = newPokemon;
+ tp.absolutePokeNumber = newPokemon.number;
+ setFormeForTrainerPokemon(tp, newPokemon);
+ tp.abilitySlot = getValidAbilitySlotFromOriginal(newPokemon, tp.abilitySlot);
+ tp.resetMoves = true;
+ }
+ }
+ }
+ }
+ this.setTrainers(currentTrainers, false);
+ }
+
+ @Override
+ public void onlyChangeTrainerLevels(Settings settings) {
+ int levelModifier = settings.getTrainersLevelModifier();
+
+ List<Trainer> currentTrainers = this.getTrainers();
+ for (Trainer t: currentTrainers) {
+ applyLevelModifierToTrainerPokemon(t, levelModifier);
+ }
+ this.setTrainers(currentTrainers, false);
+ }
+
+ @Override
+ public void addTrainerPokemon(Settings settings) {
+ int additionalNormal = settings.getAdditionalRegularTrainerPokemon();
+ int additionalImportant = settings.getAdditionalImportantTrainerPokemon();
+ int additionalBoss = settings.getAdditionalBossTrainerPokemon();
+
+ List<Trainer> currentTrainers = this.getTrainers();
+ for (Trainer t: currentTrainers) {
+ int additional;
+ if (t.isBoss()) {
+ additional = additionalBoss;
+ } else if (t.isImportant()) {
+ if (t.skipImportant()) continue;
+ additional = additionalImportant;
+ } else {
+ additional = additionalNormal;
+ }
+
+ if (additional == 0) {
+ continue;
+ }
+
+ int lowest = 100;
+ List<TrainerPokemon> potentialPokes = new ArrayList<>();
+
+ // First pass: find lowest level
+ for (TrainerPokemon tpk: t.pokemon) {
+ if (tpk.level < lowest) {
+ lowest = tpk.level;
+ }
+ }
+
+ // Second pass: find all Pokemon at lowest level
+ for (TrainerPokemon tpk: t.pokemon) {
+ if (tpk.level == lowest) {
+ potentialPokes.add(tpk);
+ }
+ }
+
+ // If a trainer can appear in a Multi Battle (i.e., a Double Battle where the enemy consists
+ // of two independent trainers), we want to be aware of that so we don't give them a team of
+ // six Pokemon and have a 6v12 battle
+ int maxPokemon = t.multiBattleStatus != Trainer.MultiBattleStatus.NEVER ? 3 : 6;
+ for (int i = 0; i < additional; i++) {
+ if (t.pokemon.size() >= maxPokemon) break;
+
+ // We want to preserve the original last Pokemon because the order is sometimes used to
+ // determine the rival's starter
+ int secondToLastIndex = t.pokemon.size() - 1;
+ TrainerPokemon newPokemon = potentialPokes.get(i % potentialPokes.size()).copy();
+
+ // Clear out the held item because we only want one Pokemon with a mega stone if we're
+ // swapping mega evolvables
+ newPokemon.heldItem = 0;
+ t.pokemon.add(secondToLastIndex, newPokemon);
+ }
+ }
+ this.setTrainers(currentTrainers, false);
+ }
+
+ @Override
+ public void doubleBattleMode() {
+ List<Trainer> currentTrainers = this.getTrainers();
+ for (Trainer t: currentTrainers) {
+ if (t.pokemon.size() != 1 || t.multiBattleStatus == Trainer.MultiBattleStatus.ALWAYS || this.trainerShouldNotGetBuffs(t)) {
+ continue;
+ }
+ t.pokemon.add(t.pokemon.get(0).copy());
+ }
+ this.setTrainers(currentTrainers, true);
+ }
+
+ private boolean trainerShouldNotGetBuffs(Trainer t) {
+ return t.tag != null && (t.tag.startsWith("RIVAL1-") || t.tag.startsWith("FRIEND1-") || t.tag.endsWith("NOTSTRONG"));
+ }
+
+ public int getRandomAbilitySlot(Pokemon pokemon) {
+ if (abilitiesPerPokemon() == 0) {
+ return 0;
+ }
+ List<Integer> abilitiesList = Arrays.asList(pokemon.ability1, pokemon.ability2, pokemon.ability3);
+ int slot = random.nextInt(this.abilitiesPerPokemon());
+ while (abilitiesList.get(slot) == 0) {
+ slot = random.nextInt(this.abilitiesPerPokemon());
+ }
+ return slot + 1;
+ }
+
+ public int getValidAbilitySlotFromOriginal(Pokemon pokemon, int originalAbilitySlot) {
+ // This is used in cases where one Trainer Pokemon evolves into another. If the unevolved Pokemon
+ // is using slot 2, but the evolved Pokemon doesn't actually have a second ability, then we
+ // want the evolved Pokemon to use slot 1 for safety's sake.
+ if (originalAbilitySlot == 2 && pokemon.ability2 == 0) {
+ return 1;
+ }
+ return originalAbilitySlot;
+ }
+
+ // MOVE DATA
+ // All randomizers don't touch move ID 165 (Struggle)
+ // They also have other exclusions where necessary to stop things glitching.
+
+ @Override
+ public void randomizeMovePowers() {
+ List<Move> moves = this.getMoves();
+ for (Move mv : moves) {
+ if (mv != null && mv.internalId != Moves.struggle && mv.power >= 10) {
+ // "Generic" damaging move to randomize power
+ if (random.nextInt(3) != 2) {
+ // "Regular" move
+ mv.power = random.nextInt(11) * 5 + 50; // 50 ... 100
+ } else {
+ // "Extreme" move
+ mv.power = random.nextInt(27) * 5 + 20; // 20 ... 150
+ }
+ // Tiny chance for massive power jumps
+ for (int i = 0; i < 2; i++) {
+ if (random.nextInt(100) == 0) {
+ mv.power += 50;
+ }
+ }
+
+ if (mv.hitCount != 1) {
+ // Divide randomized power by average hit count, round to
+ // nearest 5
+ mv.power = (int) (Math.round(mv.power / mv.hitCount / 5) * 5);
+ if (mv.power == 0) {
+ mv.power = 5;
+ }
+ }
+ }
+ }
+ }
+
+ @Override
+ public void randomizeMovePPs() {
+ List<Move> moves = this.getMoves();
+ for (Move mv : moves) {
+ if (mv != null && mv.internalId != Moves.struggle) {
+ if (random.nextInt(3) != 2) {
+ // "average" PP: 15-25
+ mv.pp = random.nextInt(3) * 5 + 15;
+ } else {
+ // "extreme" PP: 5-40
+ mv.pp = random.nextInt(8) * 5 + 5;
+ }
+ }
+ }
+ }
+
+ @Override
+ public void randomizeMoveAccuracies() {
+ List<Move> moves = this.getMoves();
+ for (Move mv : moves) {
+ if (mv != null && mv.internalId != Moves.struggle && mv.hitratio >= 5) {
+ // "Sane" accuracy randomization
+ // Broken into three tiers based on original accuracy
+ // Designed to limit the chances of 100% accurate OHKO moves and
+ // keep a decent base of 100% accurate regular moves.
+
+ if (mv.hitratio <= 50) {
+ // lowest tier (acc <= 50)
+ // new accuracy = rand(20...50) inclusive
+ // with a 10% chance to increase by 50%
+ mv.hitratio = random.nextInt(7) * 5 + 20;
+ if (random.nextInt(10) == 0) {
+ mv.hitratio = (mv.hitratio * 3 / 2) / 5 * 5;
+ }
+ } else if (mv.hitratio < 90) {
+ // middle tier (50 < acc < 90)
+ // count down from 100% to 20% in 5% increments with 20%
+ // chance to "stop" and use the current accuracy at each
+ // increment
+ // gives decent-but-not-100% accuracy most of the time
+ mv.hitratio = 100;
+ while (mv.hitratio > 20) {
+ if (random.nextInt(10) < 2) {
+ break;
+ }
+ mv.hitratio -= 5;
+ }
+ } else {
+ // highest tier (90 <= acc <= 100)
+ // count down from 100% to 20% in 5% increments with 40%
+ // chance to "stop" and use the current accuracy at each
+ // increment
+ // gives high accuracy most of the time
+ mv.hitratio = 100;
+ while (mv.hitratio > 20) {
+ if (random.nextInt(10) < 4) {
+ break;
+ }
+ mv.hitratio -= 5;
+ }
+ }
+ }
+ }
+ }
+
+ @Override
+ public void randomizeMoveTypes() {
+ List<Move> moves = this.getMoves();
+ for (Move mv : moves) {
+ if (mv != null && mv.internalId != Moves.struggle && mv.type != null) {
+ mv.type = randomType();
+ }
+ }
+ }
+
+ @Override
+ public void randomizeMoveCategory() {
+ if (!this.hasPhysicalSpecialSplit()) {
+ return;
+ }
+ List<Move> moves = this.getMoves();
+ for (Move mv : moves) {
+ if (mv != null && mv.internalId != Moves.struggle && mv.category != MoveCategory.STATUS) {
+ if (random.nextInt(2) == 0) {
+ mv.category = (mv.category == MoveCategory.PHYSICAL) ? MoveCategory.SPECIAL : MoveCategory.PHYSICAL;
+ }
+ }
+ }
+
+ }
+
+ @Override
+ public void updateMoves(Settings settings) {
+ int generation = settings.getUpdateMovesToGeneration();
+
+ List<Move> moves = this.getMoves();
+
+ if (generation >= 2 && generationOfPokemon() < 2) {
+ // gen1
+ // Karate Chop => FIGHTING (gen1)
+ updateMoveType(moves, Moves.karateChop, Type.FIGHTING);
+ // Gust => FLYING (gen1)
+ updateMoveType(moves, Moves.gust, Type.FLYING);
+ // Wing Attack => 60 power (gen1)
+ updateMovePower(moves, Moves.wingAttack, 60);
+ // Whirlwind => 100 accuracy (gen1)
+ updateMoveAccuracy(moves, Moves.whirlwind, 100);
+ // Sand Attack => GROUND (gen1)
+ updateMoveType(moves, Moves.sandAttack, Type.GROUND);
+ // Double-Edge => 120 power (gen1)
+ updateMovePower(moves, Moves.doubleEdge, 120);
+ // Move 44, Bite, becomes dark (but doesn't exist anyway)
+ // Blizzard => 70% accuracy (gen1)
+ updateMoveAccuracy(moves, Moves.blizzard, 70);
+ // Rock Throw => 90% accuracy (gen1)
+ updateMoveAccuracy(moves, Moves.rockThrow, 90);
+ // Hypnosis => 60% accuracy (gen1)
+ updateMoveAccuracy(moves, Moves.hypnosis, 60);
+ // SelfDestruct => 200power (gen1)
+ updateMovePower(moves, Moves.selfDestruct, 200);
+ // Explosion => 250 power (gen1)
+ updateMovePower(moves, Moves.explosion, 250);
+ // Dig => 60 power (gen1)
+ updateMovePower(moves, Moves.dig, 60);
+ }
+
+ if (generation >= 3 && generationOfPokemon() < 3) {
+ // Razor Wind => 100% accuracy (gen1/2)
+ updateMoveAccuracy(moves, Moves.razorWind, 100);
+ // Move 67, Low Kick, has weight-based power in gen3+
+ // Low Kick => 100% accuracy (gen1)
+ updateMoveAccuracy(moves, Moves.lowKick, 100);
+ }
+
+ if (generation >= 4 && generationOfPokemon() < 4) {
+ // Fly => 90 power (gen1/2/3)
+ updateMovePower(moves, Moves.fly, 90);
+ // Vine Whip => 15 pp (gen1/2/3)
+ updateMovePP(moves, Moves.vineWhip, 15);
+ // Absorb => 25pp (gen1/2/3)
+ updateMovePP(moves, Moves.absorb, 25);
+ // Mega Drain => 15pp (gen1/2/3)
+ updateMovePP(moves, Moves.megaDrain, 15);
+ // Dig => 80 power (gen1/2/3)
+ updateMovePower(moves, Moves.dig, 80);
+ // Recover => 10pp (gen1/2/3)
+ updateMovePP(moves, Moves.recover, 10);
+ // Flash => 100% acc (gen1/2/3)
+ updateMoveAccuracy(moves, Moves.flash, 100);
+ // Petal Dance => 90 power (gen1/2/3)
+ updateMovePower(moves, Moves.petalDance, 90);
+ // Disable => 100% accuracy (gen1-4)
+ updateMoveAccuracy(moves, Moves.disable, 80);
+ // Jump Kick => 85 power
+ updateMovePower(moves, Moves.jumpKick, 85);
+ // Hi Jump Kick => 100 power
+ updateMovePower(moves, Moves.highJumpKick, 100);
+
+ if (generationOfPokemon() >= 2) {
+ // Zap Cannon => 120 power (gen2-3)
+ updateMovePower(moves, Moves.zapCannon, 120);
+ // Outrage => 120 power (gen2-3)
+ updateMovePower(moves, Moves.outrage, 120);
+ updateMovePP(moves, Moves.outrage, 10);
+ // Giga Drain => 10pp (gen2-3)
+ updateMovePP(moves, Moves.gigaDrain, 10);
+ // Rock Smash => 40 power (gen2-3)
+ updateMovePower(moves, Moves.rockSmash, 40);
+ }
+
+ if (generationOfPokemon() == 3) {
+ // Stockpile => 20 pp
+ updateMovePP(moves, Moves.stockpile, 20);
+ // Dive => 80 power
+ updateMovePower(moves, Moves.dive, 80);
+ // Leaf Blade => 90 power
+ updateMovePower(moves, Moves.leafBlade, 90);
+ }
+ }
+
+ if (generation >= 5 && generationOfPokemon() < 5) {
+ // Bind => 85% accuracy (gen1-4)
+ updateMoveAccuracy(moves, Moves.bind, 85);
+ // Jump Kick => 10 pp, 100 power (gen1-4)
+ updateMovePP(moves, Moves.jumpKick, 10);
+ updateMovePower(moves, Moves.jumpKick, 100);
+ // Tackle => 50 power, 100% accuracy , gen1-4
+ updateMovePower(moves, Moves.tackle, 50);
+ updateMoveAccuracy(moves, Moves.tackle, 100);
+ // Wrap => 90% accuracy (gen1-4)
+ updateMoveAccuracy(moves, Moves.wrap, 90);
+ // Thrash => 120 power, 10pp (gen1-4)
+ updateMovePP(moves, Moves.thrash, 10);
+ updateMovePower(moves, Moves.thrash, 120);
+ // Disable => 100% accuracy (gen1-4)
+ updateMoveAccuracy(moves, Moves.disable, 100);
+ // Petal Dance => 120power, 10pp (gen1-4)
+ updateMovePP(moves, Moves.petalDance, 10);
+ updateMovePower(moves, Moves.petalDance, 120);
+ // Fire Spin => 35 power, 85% acc (gen1-4)
+ updateMoveAccuracy(moves, Moves.fireSpin, 85);
+ updateMovePower(moves, Moves.fireSpin, 35);
+ // Toxic => 90% accuracy (gen1-4)
+ updateMoveAccuracy(moves, Moves.toxic, 90);
+ // Clamp => 15pp, 85% acc (gen1-4)
+ updateMoveAccuracy(moves, Moves.clamp, 85);
+ updateMovePP(moves, Moves.clamp, 15);
+ // HJKick => 130 power, 10pp (gen1-4)
+ updateMovePP(moves, Moves.highJumpKick, 10);
+ updateMovePower(moves, Moves.highJumpKick, 130);
+ // Glare => 90% acc (gen1-4)
+ updateMoveAccuracy(moves, Moves.glare, 90);
+ // Poison Gas => 80% acc (gen1-4)
+ updateMoveAccuracy(moves, Moves.poisonGas, 80);
+ // Crabhammer => 90% acc (gen1-4)
+ updateMoveAccuracy(moves, Moves.crabhammer, 90);
+
+ if (generationOfPokemon() >= 2) {
+ // Curse => GHOST (gen2-4)
+ updateMoveType(moves, Moves.curse, Type.GHOST);
+ // Cotton Spore => 100% acc (gen2-4)
+ updateMoveAccuracy(moves, Moves.cottonSpore, 100);
+ // Scary Face => 100% acc (gen2-4)
+ updateMoveAccuracy(moves, Moves.scaryFace, 100);
+ // Bone Rush => 90% acc (gen2-4)
+ updateMoveAccuracy(moves, Moves.boneRush, 90);
+ // Giga Drain => 75 power (gen2-4)
+ updateMovePower(moves, Moves.gigaDrain, 75);
+ // Fury Cutter => 20 power (gen2-4)
+ updateMovePower(moves, Moves.furyCutter, 20);
+ // Future Sight => 10 pp, 100 power, 100% acc (gen2-4)
+ updateMovePP(moves, Moves.futureSight, 10);
+ updateMovePower(moves, Moves.futureSight, 100);
+ updateMoveAccuracy(moves, Moves.futureSight, 100);
+ // Whirlpool => 35 pow, 85% acc (gen2-4)
+ updateMovePower(moves, Moves.whirlpool, 35);
+ updateMoveAccuracy(moves, Moves.whirlpool, 85);
+ }
+
+ if (generationOfPokemon() >= 3) {
+ // Uproar => 90 power (gen3-4)
+ updateMovePower(moves, Moves.uproar, 90);
+ // Sand Tomb => 35 pow, 85% acc (gen3-4)
+ updateMovePower(moves, Moves.sandTomb, 35);
+ updateMoveAccuracy(moves, Moves.sandTomb, 85);
+ // Bullet Seed => 25 power (gen3-4)
+ updateMovePower(moves, Moves.bulletSeed, 25);
+ // Icicle Spear => 25 power (gen3-4)
+ updateMovePower(moves, Moves.icicleSpear, 25);
+ // Covet => 60 power (gen3-4)
+ updateMovePower(moves, Moves.covet, 60);
+ // Rock Blast => 90% acc (gen3-4)
+ updateMoveAccuracy(moves, Moves.rockBlast, 90);
+ // Doom Desire => 140 pow, 100% acc, gen3-4
+ updateMovePower(moves, Moves.doomDesire, 140);
+ updateMoveAccuracy(moves, Moves.doomDesire, 100);
+ }
+
+ if (generationOfPokemon() == 4) {
+ // Feint => 30 pow
+ updateMovePower(moves, Moves.feint, 30);
+ // Last Resort => 140 pow
+ updateMovePower(moves, Moves.lastResort, 140);
+ // Drain Punch => 10 pp, 75 pow
+ updateMovePP(moves, Moves.drainPunch, 10);
+ updateMovePower(moves, Moves.drainPunch, 75);
+ // Magma Storm => 75% acc
+ updateMoveAccuracy(moves, Moves.magmaStorm, 75);
+ }
+ }
+
+ if (generation >= 6 && generationOfPokemon() < 6) {
+ // gen 1
+ // Swords Dance 20 PP
+ updateMovePP(moves, Moves.swordsDance, 20);
+ // Whirlwind can't miss
+ updateMoveAccuracy(moves, Moves.whirlwind, perfectAccuracy);
+ // Vine Whip 25 PP, 45 Power
+ updateMovePP(moves, Moves.vineWhip, 25);
+ updateMovePower(moves, Moves.vineWhip, 45);
+ // Pin Missile 25 Power, 95% Accuracy
+ updateMovePower(moves, Moves.pinMissile, 25);
+ updateMoveAccuracy(moves, Moves.pinMissile, 95);
+ // Flamethrower 90 Power
+ updateMovePower(moves, Moves.flamethrower, 90);
+ // Hydro Pump 110 Power
+ updateMovePower(moves, Moves.hydroPump, 110);
+ // Surf 90 Power
+ updateMovePower(moves, Moves.surf, 90);
+ // Ice Beam 90 Power
+ updateMovePower(moves, Moves.iceBeam, 90);
+ // Blizzard 110 Power
+ updateMovePower(moves, Moves.blizzard, 110);
+ // Growth 20 PP
+ updateMovePP(moves, Moves.growth, 20);
+ // Thunderbolt 90 Power
+ updateMovePower(moves, Moves.thunderbolt, 90);
+ // Thunder 110 Power
+ updateMovePower(moves, Moves.thunder, 110);
+ // Minimize 10 PP
+ updateMovePP(moves, Moves.minimize, 10);
+ // Barrier 20 PP
+ updateMovePP(moves, Moves.barrier, 20);
+ // Lick 30 Power
+ updateMovePower(moves, Moves.lick, 30);
+ // Smog 30 Power
+ updateMovePower(moves, Moves.smog, 30);
+ // Fire Blast 110 Power
+ updateMovePower(moves, Moves.fireBlast, 110);
+ // Skull Bash 10 PP, 130 Power
+ updateMovePP(moves, Moves.skullBash, 10);
+ updateMovePower(moves, Moves.skullBash, 130);
+ // Glare 100% Accuracy
+ updateMoveAccuracy(moves, Moves.glare, 100);
+ // Poison Gas 90% Accuracy
+ updateMoveAccuracy(moves, Moves.poisonGas, 90);
+ // Bubble 40 Power
+ updateMovePower(moves, Moves.bubble, 40);
+ // Psywave 100% Accuracy
+ updateMoveAccuracy(moves, Moves.psywave, 100);
+ // Acid Armor 20 PP
+ updateMovePP(moves, Moves.acidArmor, 20);
+ // Crabhammer 100 Power
+ updateMovePower(moves, Moves.crabhammer, 100);
+
+ if (generationOfPokemon() >= 2) {
+ // Thief 25 PP, 60 Power
+ updateMovePP(moves, Moves.thief, 25);
+ updateMovePower(moves, Moves.thief, 60);
+ // Snore 50 Power
+ updateMovePower(moves, Moves.snore, 50);
+ // Fury Cutter 40 Power
+ updateMovePower(moves, Moves.furyCutter, 40);
+ // Future Sight 120 Power
+ updateMovePower(moves, Moves.futureSight, 120);
+ }
+
+ if (generationOfPokemon() >= 3) {
+ // Heat Wave 95 Power
+ updateMovePower(moves, Moves.heatWave, 95);
+ // Will-o-Wisp 85% Accuracy
+ updateMoveAccuracy(moves, Moves.willOWisp, 85);
+ // Smellingsalt 70 Power
+ updateMovePower(moves, Moves.smellingSalts, 70);
+ // Knock off 65 Power
+ updateMovePower(moves, Moves.knockOff, 65);
+ // Meteor Mash 90 Power, 90% Accuracy
+ updateMovePower(moves, Moves.meteorMash, 90);
+ updateMoveAccuracy(moves, Moves.meteorMash, 90);
+ // Air Cutter 60 Power
+ updateMovePower(moves, Moves.airCutter, 60);
+ // Overheat 130 Power
+ updateMovePower(moves, Moves.overheat, 130);
+ // Rock Tomb 15 PP, 60 Power, 95% Accuracy
+ updateMovePP(moves, Moves.rockTomb, 15);
+ updateMovePower(moves, Moves.rockTomb, 60);
+ updateMoveAccuracy(moves, Moves.rockTomb, 95);
+ // Extrasensory 20 PP
+ updateMovePP(moves, Moves.extrasensory, 20);
+ // Muddy Water 90 Power
+ updateMovePower(moves, Moves.muddyWater, 90);
+ // Covet 25 PP
+ updateMovePP(moves, Moves.covet, 25);
+ }
+
+ if (generationOfPokemon() >= 4) {
+ // Wake-Up Slap 70 Power
+ updateMovePower(moves, Moves.wakeUpSlap, 70);
+ // Tailwind 15 PP
+ updateMovePP(moves, Moves.tailwind, 15);
+ // Assurance 60 Power
+ updateMovePower(moves, Moves.assurance, 60);
+ // Psycho Shift 100% Accuracy
+ updateMoveAccuracy(moves, Moves.psychoShift, 100);
+ // Aura Sphere 80 Power
+ updateMovePower(moves, Moves.auraSphere, 80);
+ // Air Slash 15 PP
+ updateMovePP(moves, Moves.airSlash, 15);
+ // Dragon Pulse 85 Power
+ updateMovePower(moves, Moves.dragonPulse, 85);
+ // Power Gem 80 Power
+ updateMovePower(moves, Moves.powerGem, 80);
+ // Energy Ball 90 Power
+ updateMovePower(moves, Moves.energyBall, 90);
+ // Draco Meteor 130 Power
+ updateMovePower(moves, Moves.dracoMeteor, 130);
+ // Leaf Storm 130 Power
+ updateMovePower(moves, Moves.leafStorm, 130);
+ // Gunk Shot 80% Accuracy
+ updateMoveAccuracy(moves, Moves.gunkShot, 80);
+ // Chatter 65 Power
+ updateMovePower(moves, Moves.chatter, 65);
+ // Magma Storm 100 Power
+ updateMovePower(moves, Moves.magmaStorm, 100);
+ }
+
+ if (generationOfPokemon() == 5) {
+ // Synchronoise 120 Power
+ updateMovePower(moves, Moves.synchronoise, 120);
+ // Low Sweep 65 Power
+ updateMovePower(moves, Moves.lowSweep, 65);
+ // Hex 65 Power
+ updateMovePower(moves, Moves.hex, 65);
+ // Incinerate 60 Power
+ updateMovePower(moves, Moves.incinerate, 60);
+ // Pledges 80 Power
+ updateMovePower(moves, Moves.waterPledge, 80);
+ updateMovePower(moves, Moves.firePledge, 80);
+ updateMovePower(moves, Moves.grassPledge, 80);
+ // Struggle Bug 50 Power
+ updateMovePower(moves, Moves.struggleBug, 50);
+ // Frost Breath and Storm Throw 45 Power
+ // Crits are 2x in these games, so we need to multiply BP by 3/4
+ // Storm Throw was also updated to have a base BP of 60
+ updateMovePower(moves, Moves.frostBreath, 45);
+ updateMovePower(moves, Moves.stormThrow, 45);
+ // Sacred Sword 15 PP
+ updateMovePP(moves, Moves.sacredSword, 15);
+ // Hurricane 110 Power
+ updateMovePower(moves, Moves.hurricane, 110);
+ // Techno Blast 120 Power
+ updateMovePower(moves, Moves.technoBlast, 120);
+ }
+ }
+
+ if (generation >= 7 && generationOfPokemon() < 7) {
+ // Leech Life 80 Power, 10 PP
+ updateMovePower(moves, Moves.leechLife, 80);
+ updateMovePP(moves, Moves.leechLife, 10);
+ // Submission 20 PP
+ updateMovePP(moves, Moves.submission, 20);
+ // Tackle 40 Power
+ updateMovePower(moves, Moves.tackle, 40);
+ // Thunder Wave 90% Accuracy
+ updateMoveAccuracy(moves, Moves.thunderWave, 90);
+
+ if (generationOfPokemon() >= 2) {
+ // Swagger 85% Accuracy
+ updateMoveAccuracy(moves, Moves.swagger, 85);
+ }
+
+ if (generationOfPokemon() >= 3) {
+ // Knock Off 20 PP
+ updateMovePP(moves, Moves.knockOff, 20);
+ }
+
+ if (generationOfPokemon() >= 4) {
+ // Dark Void 50% Accuracy
+ updateMoveAccuracy(moves, Moves.darkVoid, 50);
+ // Sucker Punch 70 Power
+ updateMovePower(moves, Moves.suckerPunch, 70);
+ }
+
+ if (generationOfPokemon() == 6) {
+ // Aromatic Mist can't miss
+ updateMoveAccuracy(moves, Moves.aromaticMist, perfectAccuracy);
+ // Fell Stinger 50 Power
+ updateMovePower(moves, Moves.fellStinger, 50);
+ // Flying Press 100 Power
+ updateMovePower(moves, Moves.flyingPress, 100);
+ // Mat Block 10 PP
+ updateMovePP(moves, Moves.matBlock, 10);
+ // Mystical Fire 75 Power
+ updateMovePower(moves, Moves.mysticalFire, 75);
+ // Parabolic Charge 65 Power
+ updateMovePower(moves, Moves.parabolicCharge, 65);
+ // Topsy-Turvy can't miss
+ updateMoveAccuracy(moves, Moves.topsyTurvy, perfectAccuracy);
+ // Water Shuriken Special
+ updateMoveCategory(moves, Moves.waterShuriken, MoveCategory.SPECIAL);
+ }
+ }
+
+ if (generation >= 8 && generationOfPokemon() < 8) {
+ if (generationOfPokemon() >= 2) {
+ // Rapid Spin 50 Power
+ updateMovePower(moves, Moves.rapidSpin, 50);
+ }
+
+ if (generationOfPokemon() == 7) {
+ // Multi-Attack 120 Power
+ updateMovePower(moves, Moves.multiAttack, 120);
+ }
+ }
+ }
+
+ private Map<Integer, boolean[]> moveUpdates;
+
+ @Override
+ public void initMoveUpdates() {
+ moveUpdates = new TreeMap<>();
+ }
+
+ @Override
+ public Map<Integer, boolean[]> getMoveUpdates() {
+ return moveUpdates;
+ }
+
+ @Override
+ public void randomizeMovesLearnt(Settings settings) {
+ boolean typeThemed = settings.getMovesetsMod() == Settings.MovesetsMod.RANDOM_PREFER_SAME_TYPE;
+ boolean noBroken = settings.isBlockBrokenMovesetMoves();
+ boolean forceStartingMoves = supportsFourStartingMoves() && settings.isStartWithGuaranteedMoves();
+ int forceStartingMoveCount = settings.getGuaranteedMoveCount();
+ double goodDamagingPercentage =
+ settings.isMovesetsForceGoodDamaging() ? settings.getMovesetsGoodDamagingPercent() / 100.0 : 0;
+ boolean evolutionMovesForAll = settings.isEvolutionMovesForAll();
+
+ // Get current sets
+ Map<Integer, List<MoveLearnt>> movesets = this.getMovesLearnt();
+
+ // Build sets of moves
+ List<Move> validMoves = new ArrayList<>();
+ List<Move> validDamagingMoves = new ArrayList<>();
+ Map<Type, List<Move>> validTypeMoves = new HashMap<>();
+ Map<Type, List<Move>> validTypeDamagingMoves = new HashMap<>();
+ createSetsOfMoves(noBroken, validMoves, validDamagingMoves, validTypeMoves, validTypeDamagingMoves);
+
+ for (Integer pkmnNum : movesets.keySet()) {
+ List<Integer> learnt = new ArrayList<>();
+ List<MoveLearnt> moves = movesets.get(pkmnNum);
+ int lv1AttackingMove = 0;
+ Pokemon pkmn = findPokemonInPoolWithSpeciesID(mainPokemonListInclFormes, pkmnNum);
+ if (pkmn == null) {
+ continue;
+ }
+
+ double atkSpAtkRatio = pkmn.getAttackSpecialAttackRatio();
+
+ // 4 starting moves?
+ if (forceStartingMoves) {
+ int lv1count = 0;
+ for (MoveLearnt ml : moves) {
+ if (ml.level == 1) {
+ lv1count++;
+ }
+ }
+ if (lv1count < forceStartingMoveCount) {
+ for (int i = 0; i < forceStartingMoveCount - lv1count; i++) {
+ MoveLearnt fakeLv1 = new MoveLearnt();
+ fakeLv1.level = 1;
+ fakeLv1.move = 0;
+ moves.add(0, fakeLv1);
+ }
+ }
+ }
+
+ if (evolutionMovesForAll) {
+ if (moves.get(0).level != 0) {
+ MoveLearnt fakeEvoMove = new MoveLearnt();
+ fakeEvoMove.level = 0;
+ fakeEvoMove.move = 0;
+ moves.add(0, fakeEvoMove);
+ }
+ }
+
+ if (pkmn.actuallyCosmetic) {
+ for (int i = 0; i < moves.size(); i++) {
+ moves.get(i).move = movesets.get(pkmn.baseForme.number).get(i).move;
+ }
+ continue;
+ }
+
+ // Find last lv1 move
+ // lv1index ends up as the index of the first non-lv1 move
+ int lv1index = moves.get(0).level == 1 ? 0 : 1; // Evolution move handling (level 0 = evo move)
+ while (lv1index < moves.size() && moves.get(lv1index).level == 1) {
+ lv1index++;
+ }
+
+ // last lv1 move is 1 before lv1index
+ if (lv1index != 0) {
+ lv1index--;
+ }
+
+ // Force a certain amount of good damaging moves depending on the percentage
+ int goodDamagingLeft = (int)Math.round(goodDamagingPercentage * moves.size());
+
+ // Replace moves as needed
+ for (int i = 0; i < moves.size(); i++) {
+ // should this move be forced damaging?
+ boolean attemptDamaging = i == lv1index || goodDamagingLeft > 0;
+
+ // type themed?
+ Type typeOfMove = null;
+ if (typeThemed) {
+ double picked = random.nextDouble();
+ if ((pkmn.primaryType == Type.NORMAL && pkmn.secondaryType != null) ||
+ (pkmn.secondaryType == Type.NORMAL)) {
+
+ Type otherType = pkmn.primaryType == Type.NORMAL ? pkmn.secondaryType : pkmn.primaryType;
+
+ // Normal/OTHER: 10% normal, 30% other, 60% random
+ if (picked < 0.1) {
+ typeOfMove = Type.NORMAL;
+ } else if (picked < 0.4) {
+ typeOfMove = otherType;
+ }
+ // else random
+ } else if (pkmn.secondaryType != null) {
+ // Primary/Secondary: 20% primary, 20% secondary, 60% random
+ if (picked < 0.2) {
+ typeOfMove = pkmn.primaryType;
+ } else if (picked < 0.4) {
+ typeOfMove = pkmn.secondaryType;
+ }
+ // else random
+ } else {
+ // Primary/None: 40% primary, 60% random
+ if (picked < 0.4) {
+ typeOfMove = pkmn.primaryType;
+ }
+ // else random
+ }
+ }
+
+ // select a list to pick a move from that has at least one free
+ List<Move> pickList = validMoves;
+ if (attemptDamaging) {
+ if (typeOfMove != null) {
+ if (validTypeDamagingMoves.containsKey(typeOfMove)
+ && checkForUnusedMove(validTypeDamagingMoves.get(typeOfMove), learnt)) {
+ pickList = validTypeDamagingMoves.get(typeOfMove);
+ } else if (checkForUnusedMove(validDamagingMoves, learnt)) {
+ pickList = validDamagingMoves;
+ }
+ } else if (checkForUnusedMove(validDamagingMoves, learnt)) {
+ pickList = validDamagingMoves;
+ }
+ MoveCategory forcedCategory = random.nextDouble() < atkSpAtkRatio ? MoveCategory.PHYSICAL : MoveCategory.SPECIAL;
+ List<Move> filteredList = pickList.stream().filter(mv -> mv.category == forcedCategory).collect(Collectors.toList());
+ if (!filteredList.isEmpty() && checkForUnusedMove(filteredList, learnt)) {
+ pickList = filteredList;
+ }
+ } else if (typeOfMove != null) {
+ if (validTypeMoves.containsKey(typeOfMove)
+ && checkForUnusedMove(validTypeMoves.get(typeOfMove), learnt)) {
+ pickList = validTypeMoves.get(typeOfMove);
+ }
+ }
+
+ // now pick a move until we get a valid one
+ Move mv = pickList.get(random.nextInt(pickList.size()));
+ while (learnt.contains(mv.number)) {
+ mv = pickList.get(random.nextInt(pickList.size()));
+ }
+
+ if (i == lv1index) {
+ lv1AttackingMove = mv.number;
+ } else {
+ goodDamagingLeft--;
+ }
+ learnt.add(mv.number);
+
+ }
+
+ Collections.shuffle(learnt, random);
+ if (learnt.get(lv1index) != lv1AttackingMove) {
+ for (int i = 0; i < learnt.size(); i++) {
+ if (learnt.get(i) == lv1AttackingMove) {
+ learnt.set(i, learnt.get(lv1index));
+ learnt.set(lv1index, lv1AttackingMove);
+ break;
+ }
+ }
+ }
+
+ // write all moves for the pokemon
+ for (int i = 0; i < learnt.size(); i++) {
+ moves.get(i).move = learnt.get(i);
+ if (i == lv1index) {
+ // just in case, set this to lv1
+ moves.get(i).level = 1;
+ }
+ }
+ }
+ // Done, save
+ this.setMovesLearnt(movesets);
+
+ }
+
+ @Override
+ public void randomizeEggMoves(Settings settings) {
+ boolean typeThemed = settings.getMovesetsMod() == Settings.MovesetsMod.RANDOM_PREFER_SAME_TYPE;
+ boolean noBroken = settings.isBlockBrokenMovesetMoves();
+ double goodDamagingPercentage =
+ settings.isMovesetsForceGoodDamaging() ? settings.getMovesetsGoodDamagingPercent() / 100.0 : 0;
+
+ // Get current sets
+ Map<Integer, List<Integer>> movesets = this.getEggMoves();
+
+ // Build sets of moves
+ List<Move> validMoves = new ArrayList<>();
+ List<Move> validDamagingMoves = new ArrayList<>();
+ Map<Type, List<Move>> validTypeMoves = new HashMap<>();
+ Map<Type, List<Move>> validTypeDamagingMoves = new HashMap<>();
+ createSetsOfMoves(noBroken, validMoves, validDamagingMoves, validTypeMoves, validTypeDamagingMoves);
+
+ for (Integer pkmnNum : movesets.keySet()) {
+ List<Integer> learnt = new ArrayList<>();
+ List<Integer> moves = movesets.get(pkmnNum);
+ Pokemon pkmn = findPokemonInPoolWithSpeciesID(mainPokemonListInclFormes, pkmnNum);
+ if (pkmn == null) {
+ continue;
+ }
+
+ double atkSpAtkRatio = pkmn.getAttackSpecialAttackRatio();
+
+ if (pkmn.actuallyCosmetic) {
+ for (int i = 0; i < moves.size(); i++) {
+ moves.set(i, movesets.get(pkmn.baseForme.number).get(i));
+ }
+ continue;
+ }
+
+ // Force a certain amount of good damaging moves depending on the percentage
+ int goodDamagingLeft = (int)Math.round(goodDamagingPercentage * moves.size());
+
+ // Replace moves as needed
+ for (int i = 0; i < moves.size(); i++) {
+ // should this move be forced damaging?
+ boolean attemptDamaging = goodDamagingLeft > 0;
+
+ // type themed?
+ Type typeOfMove = null;
+ if (typeThemed) {
+ double picked = random.nextDouble();
+ if ((pkmn.primaryType == Type.NORMAL && pkmn.secondaryType != null) ||
+ (pkmn.secondaryType == Type.NORMAL)) {
+
+ Type otherType = pkmn.primaryType == Type.NORMAL ? pkmn.secondaryType : pkmn.primaryType;
+
+ // Normal/OTHER: 10% normal, 30% other, 60% random
+ if (picked < 0.1) {
+ typeOfMove = Type.NORMAL;
+ } else if (picked < 0.4) {
+ typeOfMove = otherType;
+ }
+ // else random
+ } else if (pkmn.secondaryType != null) {
+ // Primary/Secondary: 20% primary, 20% secondary, 60% random
+ if (picked < 0.2) {
+ typeOfMove = pkmn.primaryType;
+ } else if (picked < 0.4) {
+ typeOfMove = pkmn.secondaryType;
+ }
+ // else random
+ } else {
+ // Primary/None: 40% primary, 60% random
+ if (picked < 0.4) {
+ typeOfMove = pkmn.primaryType;
+ }
+ // else random
+ }
+ }
+
+ // select a list to pick a move from that has at least one free
+ List<Move> pickList = validMoves;
+ if (attemptDamaging) {
+ if (typeOfMove != null) {
+ if (validTypeDamagingMoves.containsKey(typeOfMove)
+ && checkForUnusedMove(validTypeDamagingMoves.get(typeOfMove), learnt)) {
+ pickList = validTypeDamagingMoves.get(typeOfMove);
+ } else if (checkForUnusedMove(validDamagingMoves, learnt)) {
+ pickList = validDamagingMoves;
+ }
+ } else if (checkForUnusedMove(validDamagingMoves, learnt)) {
+ pickList = validDamagingMoves;
+ }
+ MoveCategory forcedCategory = random.nextDouble() < atkSpAtkRatio ? MoveCategory.PHYSICAL : MoveCategory.SPECIAL;
+ List<Move> filteredList = pickList.stream().filter(mv -> mv.category == forcedCategory).collect(Collectors.toList());
+ if (!filteredList.isEmpty() && checkForUnusedMove(filteredList, learnt)) {
+ pickList = filteredList;
+ }
+ } else if (typeOfMove != null) {
+ if (validTypeMoves.containsKey(typeOfMove)
+ && checkForUnusedMove(validTypeMoves.get(typeOfMove), learnt)) {
+ pickList = validTypeMoves.get(typeOfMove);
+ }
+ }
+
+ // now pick a move until we get a valid one
+ Move mv = pickList.get(random.nextInt(pickList.size()));
+ while (learnt.contains(mv.number)) {
+ mv = pickList.get(random.nextInt(pickList.size()));
+ }
+
+ goodDamagingLeft--;
+ learnt.add(mv.number);
+ }
+
+ // write all moves for the pokemon
+ Collections.shuffle(learnt, random);
+ for (int i = 0; i < learnt.size(); i++) {
+ moves.set(i, learnt.get(i));
+ }
+ }
+ // Done, save
+ this.setEggMoves(movesets);
+ }
+
+ private void createSetsOfMoves(boolean noBroken, List<Move> validMoves, List<Move> validDamagingMoves,
+ Map<Type, List<Move>> validTypeMoves, Map<Type, List<Move>> validTypeDamagingMoves) {
+ List<Move> allMoves = this.getMoves();
+ List<Integer> hms = this.getHMMoves();
+ Set<Integer> allBanned = new HashSet<Integer>(noBroken ? this.getGameBreakingMoves() : Collections.EMPTY_SET);
+ allBanned.addAll(hms);
+ allBanned.addAll(this.getMovesBannedFromLevelup());
+ allBanned.addAll(GlobalConstants.zMoves);
+ allBanned.addAll(this.getIllegalMoves());
+
+ for (Move mv : allMoves) {
+ if (mv != null && !GlobalConstants.bannedRandomMoves[mv.number] && !allBanned.contains(mv.number)) {
+ validMoves.add(mv);
+ if (mv.type != null) {
+ if (!validTypeMoves.containsKey(mv.type)) {
+ validTypeMoves.put(mv.type, new ArrayList<>());
+ }
+ validTypeMoves.get(mv.type).add(mv);
+ }
+
+ if (!GlobalConstants.bannedForDamagingMove[mv.number]) {
+ if ((mv.power * mv.hitCount) >= 2 * GlobalConstants.MIN_DAMAGING_MOVE_POWER
+ || ((mv.power * mv.hitCount) >= GlobalConstants.MIN_DAMAGING_MOVE_POWER && (mv.hitratio >= 90 || mv.hitratio == perfectAccuracy))) {
+ validDamagingMoves.add(mv);
+ if (mv.type != null) {
+ if (!validTypeDamagingMoves.containsKey(mv.type)) {
+ validTypeDamagingMoves.put(mv.type, new ArrayList<>());
+ }
+ validTypeDamagingMoves.get(mv.type).add(mv);
+ }
+ }
+ }
+ }
+ }
+
+ Map<Type,Double> avgTypePowers = new TreeMap<>();
+ double totalAvgPower = 0;
+
+ for (Type type: validTypeMoves.keySet()) {
+ List<Move> typeMoves = validTypeMoves.get(type);
+ int attackingSum = 0;
+ for (Move typeMove: typeMoves) {
+ if (typeMove.power > 0) {
+ attackingSum += (typeMove.power * typeMove.hitCount);
+ }
+ }
+ double avgTypePower = (double)attackingSum / (double)typeMoves.size();
+ avgTypePowers.put(type, avgTypePower);
+ totalAvgPower += (avgTypePower);
+ }
+
+ totalAvgPower /= (double)validTypeMoves.keySet().size();
+
+ // Want the average power of each type to be within 25% both directions
+ double minAvg = totalAvgPower * 0.75;
+ double maxAvg = totalAvgPower * 1.25;
+
+ // Add extra moves to type lists outside of the range to balance the average power of each type
+
+ for (Type type: avgTypePowers.keySet()) {
+ double avgPowerForType = avgTypePowers.get(type);
+ List<Move> typeMoves = validTypeMoves.get(type);
+ List<Move> alreadyPicked = new ArrayList<>();
+ int iterLoops = 0;
+ while (avgPowerForType < minAvg && iterLoops < 10000) {
+ final double finalAvgPowerForType = avgPowerForType;
+ List<Move> strongerThanAvgTypeMoves = typeMoves
+ .stream()
+ .filter(mv -> mv.power * mv.hitCount > finalAvgPowerForType)
+ .collect(Collectors.toList());
+ if (strongerThanAvgTypeMoves.isEmpty()) break;
+ if (alreadyPicked.containsAll(strongerThanAvgTypeMoves)) {
+ alreadyPicked = new ArrayList<>();
+ } else {
+ strongerThanAvgTypeMoves.removeAll(alreadyPicked);
+ }
+ Move extraMove = strongerThanAvgTypeMoves.get(random.nextInt(strongerThanAvgTypeMoves.size()));
+ avgPowerForType = (avgPowerForType * typeMoves.size() + extraMove.power * extraMove.hitCount)
+ / (typeMoves.size() + 1);
+ typeMoves.add(extraMove);
+ alreadyPicked.add(extraMove);
+ iterLoops++;
+ }
+ iterLoops = 0;
+ while (avgPowerForType > maxAvg && iterLoops < 10000) {
+ final double finalAvgPowerForType = avgPowerForType;
+ List<Move> weakerThanAvgTypeMoves = typeMoves
+ .stream()
+ .filter(mv -> mv.power * mv.hitCount < finalAvgPowerForType)
+ .collect(Collectors.toList());
+ if (weakerThanAvgTypeMoves.isEmpty()) break;
+ if (alreadyPicked.containsAll(weakerThanAvgTypeMoves)) {
+ alreadyPicked = new ArrayList<>();
+ } else {
+ weakerThanAvgTypeMoves.removeAll(alreadyPicked);
+ }
+ Move extraMove = weakerThanAvgTypeMoves.get(random.nextInt(weakerThanAvgTypeMoves.size()));
+ avgPowerForType = (avgPowerForType * typeMoves.size() + extraMove.power * extraMove.hitCount)
+ / (typeMoves.size() + 1);
+ typeMoves.add(extraMove);
+ alreadyPicked.add(extraMove);
+ iterLoops++;
+ }
+ }
+ }
+
+ @Override
+ public void orderDamagingMovesByDamage() {
+ Map<Integer, List<MoveLearnt>> movesets = this.getMovesLearnt();
+ List<Move> allMoves = this.getMoves();
+ for (Integer pkmn : movesets.keySet()) {
+ List<MoveLearnt> moves = movesets.get(pkmn);
+
+ // Build up a list of damaging moves and their positions
+ List<Integer> damagingMoveIndices = new ArrayList<>();
+ List<Move> damagingMoves = new ArrayList<>();
+ for (int i = 0; i < moves.size(); i++) {
+ if (moves.get(i).level == 0) continue; // Don't reorder evolution move
+ Move mv = allMoves.get(moves.get(i).move);
+ if (mv.power > 1) {
+ // considered a damaging move for this purpose
+ damagingMoveIndices.add(i);
+ damagingMoves.add(mv);
+ }
+ }
+
+ // Ties should be sorted randomly, so shuffle the list first.
+ Collections.shuffle(damagingMoves, random);
+
+ // Sort the damaging moves by power
+ damagingMoves.sort(Comparator.comparingDouble(m -> m.power * m.hitCount));
+
+ // Reassign damaging moves in the ordered positions
+ for (int i = 0; i < damagingMoves.size(); i++) {
+ moves.get(damagingMoveIndices.get(i)).move = damagingMoves.get(i).number;
+ }
+ }
+
+ // Done, save
+ this.setMovesLearnt(movesets);
+ }
+
+ @Override
+ public void metronomeOnlyMode() {
+
+ // movesets
+ Map<Integer, List<MoveLearnt>> movesets = this.getMovesLearnt();
+
+ MoveLearnt metronomeML = new MoveLearnt();
+ metronomeML.level = 1;
+ metronomeML.move = Moves.metronome;
+
+ for (List<MoveLearnt> ms : movesets.values()) {
+ if (ms != null && ms.size() > 0) {
+ ms.clear();
+ ms.add(metronomeML);
+ }
+ }
+
+ this.setMovesLearnt(movesets);
+
+ // trainers
+ // run this to remove all custom non-Metronome moves
+ List<Trainer> trainers = this.getTrainers();
+
+ for (Trainer t : trainers) {
+ for (TrainerPokemon tpk : t.pokemon) {
+ tpk.resetMoves = true;
+ }
+ }
+
+ this.setTrainers(trainers, false);
+
+ // tms
+ List<Integer> tmMoves = this.getTMMoves();
+
+ for (int i = 0; i < tmMoves.size(); i++) {
+ tmMoves.set(i, Moves.metronome);
+ }
+
+ this.setTMMoves(tmMoves);
+
+ // movetutors
+ if (this.hasMoveTutors()) {
+ List<Integer> mtMoves = this.getMoveTutorMoves();
+
+ for (int i = 0; i < mtMoves.size(); i++) {
+ mtMoves.set(i, Moves.metronome);
+ }
+
+ this.setMoveTutorMoves(mtMoves);
+ }
+
+ // move tweaks
+ List<Move> moveData = this.getMoves();
+
+ Move metronome = moveData.get(Moves.metronome);
+
+ metronome.pp = 40;
+
+ List<Integer> hms = this.getHMMoves();
+
+ for (int hm : hms) {
+ Move thisHM = moveData.get(hm);
+ thisHM.pp = 0;
+ }
+ }
+
+ @Override
+ public void customStarters(Settings settings) {
+ int[] customStarters = settings.getCustomStarters();
+ boolean allowAltFormes = settings.isAllowStarterAltFormes();
+
+ List<Pokemon> romPokemon = getPokemonInclFormes()
+ .stream()
+ .filter(pk -> pk == null || !pk.actuallyCosmetic)
+ .collect(Collectors.toList());
+
+ pickedStarters = new ArrayList<>();
+ Pokemon pkmn1 = romPokemon.get(customStarters[0]);
+ pickedStarters.add(pkmn1);
+ Pokemon pkmn2 = romPokemon.get(customStarters[1]);
+ pickedStarters.add(pkmn2);
+
+ if (isYellow()) {
+ setStarters(pickedStarters);
+ } else {
+ Pokemon pkmn3 = romPokemon.get(customStarters[2]);
+ pickedStarters.add(pkmn3);
+ if (starterCount() > 3) {
+ for (int i = 3; i < starterCount(); i++) {
+ Pokemon pkmn = random2EvosPokemon(allowAltFormes);
+ while (pickedStarters.contains(pkmn)) {
+ pkmn = random2EvosPokemon(allowAltFormes);
+ }
+ pickedStarters.add(pkmn);
+ }
+ setStarters(pickedStarters);
+ } else {
+ setStarters(pickedStarters);
+ }
+ }
+ }
+
+ @Override
+ public void randomizeStarters(Settings settings) {
+ boolean abilitiesUnchanged = settings.getAbilitiesMod() == Settings.AbilitiesMod.UNCHANGED;
+ boolean allowAltFormes = settings.isAllowStarterAltFormes();
+ boolean banIrregularAltFormes = settings.isBanIrregularAltFormes();
+
+ int starterCount = starterCount();
+ pickedStarters = new ArrayList<>();
+ List<Pokemon> banned = getBannedFormesForPlayerPokemon();
+ if (abilitiesUnchanged) {
+ List<Pokemon> abilityDependentFormes = getAbilityDependentFormes();
+ banned.addAll(abilityDependentFormes);
+ }
+ if (banIrregularAltFormes) {
+ banned.addAll(getIrregularFormes());
+ }
+ for (int i = 0; i < starterCount; i++) {
+ Pokemon pkmn = allowAltFormes ? randomPokemonInclFormes() : randomPokemon();
+ while (pickedStarters.contains(pkmn) || banned.contains(pkmn) || pkmn.actuallyCosmetic) {
+ pkmn = allowAltFormes ? randomPokemonInclFormes() : randomPokemon();
+ }
+ pickedStarters.add(pkmn);
+ }
+ setStarters(pickedStarters);
+ }
+
+ @Override
+ public void randomizeBasicTwoEvosStarters(Settings settings) {
+ boolean abilitiesUnchanged = settings.getAbilitiesMod() == Settings.AbilitiesMod.UNCHANGED;
+ boolean allowAltFormes = settings.isAllowStarterAltFormes();
+ boolean banIrregularAltFormes = settings.isBanIrregularAltFormes();
+
+ int starterCount = starterCount();
+ pickedStarters = new ArrayList<>();
+ List<Pokemon> banned = getBannedFormesForPlayerPokemon();
+ if (abilitiesUnchanged) {
+ List<Pokemon> abilityDependentFormes = getAbilityDependentFormes();
+ banned.addAll(abilityDependentFormes);
+ }
+ if (banIrregularAltFormes) {
+ banned.addAll(getIrregularFormes());
+ }
+ for (int i = 0; i < starterCount; i++) {
+ Pokemon pkmn = random2EvosPokemon(allowAltFormes);
+ while (pickedStarters.contains(pkmn) || banned.contains(pkmn)) {
+ pkmn = random2EvosPokemon(allowAltFormes);
+ }
+ pickedStarters.add(pkmn);
+ }
+ setStarters(pickedStarters);
+ }
+
+ @Override
+ public List<Pokemon> getPickedStarters() {
+ return pickedStarters;
+ }
+
+
+ @Override
+ public void randomizeStaticPokemon(Settings settings) {
+ boolean swapLegendaries = settings.getStaticPokemonMod() == Settings.StaticPokemonMod.RANDOM_MATCHING;
+ boolean similarStrength = settings.getStaticPokemonMod() == Settings.StaticPokemonMod.SIMILAR_STRENGTH;
+ boolean limitMainGameLegendaries = settings.isLimitMainGameLegendaries();
+ boolean limit600 = settings.isLimit600();
+ boolean allowAltFormes = settings.isAllowStaticAltFormes();
+ boolean banIrregularAltFormes = settings.isBanIrregularAltFormes();
+ boolean swapMegaEvos = settings.isSwapStaticMegaEvos();
+ boolean abilitiesAreRandomized = settings.getAbilitiesMod() == Settings.AbilitiesMod.RANDOMIZE;
+ int levelModifier = settings.isStaticLevelModified() ? settings.getStaticLevelModifier() : 0;
+ boolean correctStaticMusic = settings.isCorrectStaticMusic();
+
+ // Load
+ checkPokemonRestrictions();
+ List<StaticEncounter> currentStaticPokemon = this.getStaticPokemon();
+ List<StaticEncounter> replacements = new ArrayList<>();
+ List<Pokemon> banned = this.bannedForStaticPokemon();
+ banned.addAll(this.getBannedFormesForPlayerPokemon());
+ if (!abilitiesAreRandomized) {
+ List<Pokemon> abilityDependentFormes = getAbilityDependentFormes();
+ banned.addAll(abilityDependentFormes);
+ }
+ if (banIrregularAltFormes) {
+ banned.addAll(getIrregularFormes());
+ }
+ boolean reallySwapMegaEvos = forceSwapStaticMegaEvos() || swapMegaEvos;
+
+ Map<Integer,Integer> specialMusicStaticChanges = new HashMap<>();
+ List<Integer> changeMusicStatics = new ArrayList<>();
+ if (correctStaticMusic) {
+ changeMusicStatics = getSpecialMusicStatics();
+ }
+
+ if (swapLegendaries) {
+ List<Pokemon> legendariesLeft = new ArrayList<>(onlyLegendaryList);
+ if (allowAltFormes) {
+ legendariesLeft.addAll(onlyLegendaryAltsList);
+ legendariesLeft =
+ legendariesLeft
+ .stream()
+ .filter(pk -> !pk.actuallyCosmetic)
+ .collect(Collectors.toList());
+ }
+ List<Pokemon> nonlegsLeft = new ArrayList<>(noLegendaryList);
+ if (allowAltFormes) {
+ nonlegsLeft.addAll(noLegendaryAltsList);
+ nonlegsLeft =
+ nonlegsLeft
+ .stream()
+ .filter(pk -> !pk.actuallyCosmetic)
+ .collect(Collectors.toList());
+ }
+ List<Pokemon> ultraBeastsLeft = new ArrayList<>(ultraBeastList);
+ legendariesLeft.removeAll(banned);
+ nonlegsLeft.removeAll(banned);
+ ultraBeastsLeft.removeAll(banned);
+
+ // Full pools for easier refilling later
+ List<Pokemon> legendariesPool = new ArrayList<>(legendariesLeft);
+ List<Pokemon> nonlegsPool = new ArrayList<>(nonlegsLeft);
+ List<Pokemon> ultraBeastsPool = new ArrayList<>(ultraBeastsLeft);
+
+ for (StaticEncounter old : currentStaticPokemon) {
+ StaticEncounter newStatic = cloneStaticEncounter(old);
+ Pokemon newPK;
+ if (old.pkmn.isLegendary()) {
+ if (reallySwapMegaEvos && old.canMegaEvolve()) {
+ newPK = getMegaEvoPokemon(onlyLegendaryList, legendariesLeft, newStatic);
+ } else {
+ if (old.restrictedPool) {
+ newPK = getRestrictedPokemon(legendariesPool, legendariesLeft, old);
+ } else {
+ newPK = legendariesLeft.remove(this.random.nextInt(legendariesLeft.size()));
+ }
+ }
+
+ setPokemonAndFormeForStaticEncounter(newStatic, newPK);
+
+ if (legendariesLeft.size() == 0) {
+ legendariesLeft.addAll(legendariesPool);
+ }
+ } else if (ultraBeastList.contains(old.pkmn)) {
+ if (old.restrictedPool) {
+ newPK = getRestrictedPokemon(ultraBeastsPool, ultraBeastsLeft, old);
+ } else {
+ newPK = ultraBeastsLeft.remove(this.random.nextInt(ultraBeastsLeft.size()));
+ }
+
+ setPokemonAndFormeForStaticEncounter(newStatic, newPK);
+
+ if (ultraBeastsLeft.size() == 0) {
+ ultraBeastsLeft.addAll(ultraBeastsPool);
+ }
+ } else {
+ if (reallySwapMegaEvos && old.canMegaEvolve()) {
+ newPK = getMegaEvoPokemon(noLegendaryList, nonlegsLeft, newStatic);
+ } else {
+ if (old.restrictedPool) {
+ newPK = getRestrictedPokemon(nonlegsPool, nonlegsLeft, old);
+ } else {
+ newPK = nonlegsLeft.remove(this.random.nextInt(nonlegsLeft.size()));
+ }
+ }
+ setPokemonAndFormeForStaticEncounter(newStatic, newPK);
+
+ if (nonlegsLeft.size() == 0) {
+ nonlegsLeft.addAll(nonlegsPool);
+ }
+ }
+ replacements.add(newStatic);
+ if (changeMusicStatics.contains(old.pkmn.number)) {
+ specialMusicStaticChanges.put(old.pkmn.number, newPK.number);
+ }
+ }
+ } else if (similarStrength) {
+ List<Pokemon> listInclFormesExclCosmetics =
+ mainPokemonListInclFormes
+ .stream()
+ .filter(pk -> !pk.actuallyCosmetic)
+ .collect(Collectors.toList());
+ List<Pokemon> pokemonLeft = new ArrayList<>(!allowAltFormes ? mainPokemonList : listInclFormesExclCosmetics);
+ pokemonLeft.removeAll(banned);
+
+ List<Pokemon> pokemonPool = new ArrayList<>(pokemonLeft);
+
+ List<Integer> mainGameLegendaries = getMainGameLegendaries();
+ for (StaticEncounter old : currentStaticPokemon) {
+ StaticEncounter newStatic = cloneStaticEncounter(old);
+ Pokemon newPK;
+ Pokemon oldPK = old.pkmn;
+ if (old.forme > 0) {
+ oldPK = getAltFormeOfPokemon(oldPK, old.forme);
+ }
+ Integer oldBST = oldPK.bstForPowerLevels();
+ if (oldBST >= 600 && limit600) {
+ if (reallySwapMegaEvos && old.canMegaEvolve()) {
+ newPK = getMegaEvoPokemon(mainPokemonList, pokemonLeft, newStatic);
+ } else {
+ if (old.restrictedPool) {
+ newPK = getRestrictedPokemon(pokemonPool, pokemonLeft, old);
+ } else {
+ newPK = pokemonLeft.remove(this.random.nextInt(pokemonLeft.size()));
+ }
+ }
+ setPokemonAndFormeForStaticEncounter(newStatic, newPK);
+ } else {
+ boolean limitBST = oldPK.baseForme == null ?
+ limitMainGameLegendaries && mainGameLegendaries.contains(oldPK.number) :
+ limitMainGameLegendaries && mainGameLegendaries.contains(oldPK.baseForme.number);
+ if (reallySwapMegaEvos && old.canMegaEvolve()) {
+ List<Pokemon> megaEvoPokemonLeft =
+ megaEvolutionsList
+ .stream()
+ .filter(mega -> mega.method == 1)
+ .map(mega -> mega.from)
+ .distinct()
+ .filter(pokemonLeft::contains)
+ .collect(Collectors.toList());
+ if (megaEvoPokemonLeft.isEmpty()) {
+ megaEvoPokemonLeft =
+ megaEvolutionsList
+ .stream()
+ .filter(mega -> mega.method == 1)
+ .map(mega -> mega.from)
+ .distinct()
+ .filter(mainPokemonList::contains)
+ .collect(Collectors.toList());
+ }
+ newPK = pickStaticPowerLvlReplacement(
+ megaEvoPokemonLeft,
+ oldPK,
+ true,
+ limitBST);
+ newStatic.heldItem = newPK
+ .megaEvolutionsFrom
+ .get(this.random.nextInt(newPK.megaEvolutionsFrom.size()))
+ .argument;
+ } else {
+ if (old.restrictedPool) {
+ List<Pokemon> restrictedPool = pokemonLeft
+ .stream()
+ .filter(pk -> old.restrictedList.contains(pk))
+ .collect(Collectors.toList());
+ if (restrictedPool.isEmpty()) {
+ restrictedPool = pokemonPool
+ .stream()
+ .filter(pk -> old.restrictedList.contains(pk))
+ .collect(Collectors.toList());
+ }
+ newPK = pickStaticPowerLvlReplacement(
+ restrictedPool,
+ oldPK,
+ false, // Allow same Pokemon just in case
+ limitBST);
+ } else {
+ newPK = pickStaticPowerLvlReplacement(
+ pokemonLeft,
+ oldPK,
+ true,
+ limitBST);
+ }
+ }
+ pokemonLeft.remove(newPK);
+ setPokemonAndFormeForStaticEncounter(newStatic, newPK);
+ }
+
+ if (pokemonLeft.size() == 0) {
+ pokemonLeft.addAll(pokemonPool);
+ }
+ replacements.add(newStatic);
+ if (changeMusicStatics.contains(old.pkmn.number)) {
+ specialMusicStaticChanges.put(old.pkmn.number, newPK.number);
+ }
+ }
+ } else { // Completely random
+ List<Pokemon> listInclFormesExclCosmetics =
+ mainPokemonListInclFormes
+ .stream()
+ .filter(pk -> !pk.actuallyCosmetic)
+ .collect(Collectors.toList());
+ List<Pokemon> pokemonLeft = new ArrayList<>(!allowAltFormes ? mainPokemonList : listInclFormesExclCosmetics);
+ pokemonLeft.removeAll(banned);
+
+ List<Pokemon> pokemonPool = new ArrayList<>(pokemonLeft);
+
+ for (StaticEncounter old : currentStaticPokemon) {
+ StaticEncounter newStatic = cloneStaticEncounter(old);
+ Pokemon newPK;
+ if (reallySwapMegaEvos && old.canMegaEvolve()) {
+ newPK = getMegaEvoPokemon(mainPokemonList, pokemonLeft, newStatic);
+ } else {
+ if (old.restrictedPool) {
+ newPK = getRestrictedPokemon(pokemonPool, pokemonLeft, old);
+ } else {
+ newPK = pokemonLeft.remove(this.random.nextInt(pokemonLeft.size()));
+ }
+ }
+ pokemonLeft.remove(newPK);
+ setPokemonAndFormeForStaticEncounter(newStatic, newPK);
+ if (pokemonLeft.size() == 0) {
+ pokemonLeft.addAll(pokemonPool);
+ }
+ replacements.add(newStatic);
+ if (changeMusicStatics.contains(old.pkmn.number)) {
+ specialMusicStaticChanges.put(old.pkmn.number, newPK.number);
+ }
+ }
+ }
+
+ if (levelModifier != 0) {
+ for (StaticEncounter se : replacements) {
+ if (!se.isEgg) {
+ se.level = Math.min(100, (int) Math.round(se.level * (1 + levelModifier / 100.0)));
+ se.maxLevel = Math.min(100, (int) Math.round(se.maxLevel * (1 + levelModifier / 100.0)));
+ for (StaticEncounter linkedStatic : se.linkedEncounters) {
+ if (!linkedStatic.isEgg) {
+ linkedStatic.level = Math.min(100, (int) Math.round(linkedStatic.level * (1 + levelModifier / 100.0)));
+ linkedStatic.maxLevel = Math.min(100, (int) Math.round(linkedStatic.maxLevel * (1 + levelModifier / 100.0)));
+ }
+ }
+ }
+ }
+ }
+
+ if (specialMusicStaticChanges.size() > 0) {
+ applyCorrectStaticMusic(specialMusicStaticChanges);
+ }
+
+ // Save
+ this.setStaticPokemon(replacements);
+ }
+
+ private Pokemon getRestrictedPokemon(List<Pokemon> fullList, List<Pokemon> pokemonLeft, StaticEncounter old) {
+ Pokemon newPK;
+ List<Pokemon> restrictedPool = pokemonLeft.stream().filter(pk -> old.restrictedList.contains(pk)).collect(Collectors.toList());
+ if (restrictedPool.isEmpty()) {
+ restrictedPool = fullList
+ .stream()
+ .filter(pk -> old.restrictedList.contains(pk))
+ .collect(Collectors.toList());
+ }
+ newPK = restrictedPool.remove(this.random.nextInt(restrictedPool.size()));
+ pokemonLeft.remove(newPK);
+ return newPK;
+ }
+
+ @Override
+ public void onlyChangeStaticLevels(Settings settings) {
+ int levelModifier = settings.getStaticLevelModifier();
+
+ List<StaticEncounter> currentStaticPokemon = this.getStaticPokemon();
+ for (StaticEncounter se : currentStaticPokemon) {
+ if (!se.isEgg) {
+ se.level = Math.min(100, (int) Math.round(se.level * (1 + levelModifier / 100.0)));
+ for (StaticEncounter linkedStatic : se.linkedEncounters) {
+ if (!linkedStatic.isEgg) {
+ linkedStatic.level = Math.min(100, (int) Math.round(linkedStatic.level * (1 + levelModifier / 100.0)));
+ }
+ }
+ }
+ setPokemonAndFormeForStaticEncounter(se, se.pkmn);
+ }
+ this.setStaticPokemon(currentStaticPokemon);
+ }
+
+ private StaticEncounter cloneStaticEncounter(StaticEncounter old) {
+ StaticEncounter newStatic = new StaticEncounter();
+ newStatic.pkmn = old.pkmn;
+ newStatic.level = old.level;
+ newStatic.maxLevel = old.maxLevel;
+ newStatic.heldItem = old.heldItem;
+ newStatic.isEgg = old.isEgg;
+ newStatic.resetMoves = true;
+ for (StaticEncounter oldLinked : old.linkedEncounters) {
+ StaticEncounter newLinked = new StaticEncounter();
+ newLinked.pkmn = oldLinked.pkmn;
+ newLinked.level = oldLinked.level;
+ newLinked.maxLevel = oldLinked.maxLevel;
+ newLinked.heldItem = oldLinked.heldItem;
+ newLinked.isEgg = oldLinked.isEgg;
+ newLinked.resetMoves = true;
+ newStatic.linkedEncounters.add(newLinked);
+ }
+ return newStatic;
+ }
+
+ private void setPokemonAndFormeForStaticEncounter(StaticEncounter newStatic, Pokemon pk) {
+ boolean checkCosmetics = true;
+ Pokemon newPK = pk;
+ int newForme = 0;
+ if (pk.formeNumber > 0) {
+ newForme = pk.formeNumber;
+ newPK = pk.baseForme;
+ checkCosmetics = false;
+ }
+ if (checkCosmetics && pk.cosmeticForms > 0) {
+ newForme = pk.getCosmeticFormNumber(this.random.nextInt(pk.cosmeticForms));
+ } else if (!checkCosmetics && pk.cosmeticForms > 0) {
+ newForme += pk.getCosmeticFormNumber(this.random.nextInt(pk.cosmeticForms));
+ }
+ newStatic.pkmn = newPK;
+ newStatic.forme = newForme;
+ for (StaticEncounter linked : newStatic.linkedEncounters) {
+ linked.pkmn = newPK;
+ linked.forme = newForme;
+ }
+ }
+
+ private void setFormeForStaticEncounter(StaticEncounter newStatic, Pokemon pk) {
+ boolean checkCosmetics = true;
+ newStatic.forme = 0;
+ if (pk.formeNumber > 0) {
+ newStatic.forme = pk.formeNumber;
+ newStatic.pkmn = pk.baseForme;
+ checkCosmetics = false;
+ }
+ if (checkCosmetics && newStatic.pkmn.cosmeticForms > 0) {
+ newStatic.forme = newStatic.pkmn.getCosmeticFormNumber(this.random.nextInt(newStatic.pkmn.cosmeticForms));
+ } else if (!checkCosmetics && pk.cosmeticForms > 0) {
+ newStatic.forme += pk.getCosmeticFormNumber(this.random.nextInt(pk.cosmeticForms));
+ }
+ }
+
+ private Pokemon getMegaEvoPokemon(List<Pokemon> fullList, List<Pokemon> pokemonLeft, StaticEncounter newStatic) {
+ List<MegaEvolution> megaEvos = megaEvolutionsList;
+ List<Pokemon> megaEvoPokemon =
+ megaEvos
+ .stream()
+ .filter(mega -> mega.method == 1)
+ .map(mega -> mega.from)
+ .distinct()
+ .collect(Collectors.toList());
+ Pokemon newPK;
+ List<Pokemon> megaEvoPokemonLeft =
+ megaEvoPokemon
+ .stream()
+ .filter(pokemonLeft::contains)
+ .collect(Collectors.toList());
+ if (megaEvoPokemonLeft.isEmpty()) {
+ megaEvoPokemonLeft = megaEvoPokemon
+ .stream()
+ .filter(fullList::contains)
+ .collect(Collectors.toList());
+ }
+ newPK = megaEvoPokemonLeft.remove(this.random.nextInt(megaEvoPokemonLeft.size()));
+ pokemonLeft.remove(newPK);
+ newStatic.heldItem = newPK
+ .megaEvolutionsFrom
+ .get(this.random.nextInt(newPK.megaEvolutionsFrom.size()))
+ .argument;
+ return newPK;
+ }
+
+ @Override
+ public void randomizeTMMoves(Settings settings) {
+ boolean noBroken = settings.isBlockBrokenTMMoves();
+ boolean preserveField = settings.isKeepFieldMoveTMs();
+ double goodDamagingPercentage = settings.isTmsForceGoodDamaging() ? settings.getTmsGoodDamagingPercent() / 100.0 : 0;
+
+ // Pick some random TM moves.
+ int tmCount = this.getTMCount();
+ List<Move> allMoves = this.getMoves();
+ List<Integer> hms = this.getHMMoves();
+ List<Integer> oldTMs = this.getTMMoves();
+ @SuppressWarnings("unchecked")
+ List<Integer> banned = new ArrayList<Integer>(noBroken ? this.getGameBreakingMoves() : Collections.EMPTY_LIST);
+ banned.addAll(getMovesBannedFromLevelup());
+ banned.addAll(this.getIllegalMoves());
+ // field moves?
+ List<Integer> fieldMoves = this.getFieldMoves();
+ int preservedFieldMoveCount = 0;
+
+ if (preserveField) {
+ List<Integer> banExistingField = new ArrayList<>(oldTMs);
+ banExistingField.retainAll(fieldMoves);
+ preservedFieldMoveCount = banExistingField.size();
+ banned.addAll(banExistingField);
+ }
+
+ // Determine which moves are pickable
+ List<Move> usableMoves = new ArrayList<>(allMoves);
+ usableMoves.remove(0); // remove null entry
+ Set<Move> unusableMoves = new HashSet<>();
+ Set<Move> unusableDamagingMoves = new HashSet<>();
+
+ for (Move mv : usableMoves) {
+ if (GlobalConstants.bannedRandomMoves[mv.number] || GlobalConstants.zMoves.contains(mv.number) ||
+ hms.contains(mv.number) || banned.contains(mv.number)) {
+ unusableMoves.add(mv);
+ } else if (GlobalConstants.bannedForDamagingMove[mv.number]
+ || (mv.power * mv.hitCount) < GlobalConstants.MIN_DAMAGING_MOVE_POWER
+ || ((mv.power * mv.hitCount) < GlobalConstants.MIN_DAMAGING_MOVE_POWER * 2 && mv.hitratio < 90 && mv.hitratio != perfectAccuracy)) {
+ unusableDamagingMoves.add(mv);
+ }
+ }
+
+ usableMoves.removeAll(unusableMoves);
+ List<Move> usableDamagingMoves = new ArrayList<>(usableMoves);
+ usableDamagingMoves.removeAll(unusableDamagingMoves);
+
+ // pick (tmCount - preservedFieldMoveCount) moves
+ List<Integer> pickedMoves = new ArrayList<>();
+
+ // Force a certain amount of good damaging moves depending on the percentage
+ int goodDamagingLeft = (int)Math.round(goodDamagingPercentage * (tmCount - preservedFieldMoveCount));
+
+ for (int i = 0; i < tmCount - preservedFieldMoveCount; i++) {
+ Move chosenMove;
+ if (goodDamagingLeft > 0 && usableDamagingMoves.size() > 0) {
+ chosenMove = usableDamagingMoves.get(random.nextInt(usableDamagingMoves.size()));
+ } else {
+ chosenMove = usableMoves.get(random.nextInt(usableMoves.size()));
+ }
+ pickedMoves.add(chosenMove.number);
+ usableMoves.remove(chosenMove);
+ usableDamagingMoves.remove(chosenMove);
+ goodDamagingLeft--;
+ }
+
+ // shuffle the picked moves because high goodDamagingPercentage
+ // will bias them towards early numbers otherwise
+
+ Collections.shuffle(pickedMoves, random);
+
+ // finally, distribute them as tms
+ int pickedMoveIndex = 0;
+ List<Integer> newTMs = new ArrayList<>();
+
+ for (int i = 0; i < tmCount; i++) {
+ if (preserveField && fieldMoves.contains(oldTMs.get(i))) {
+ newTMs.add(oldTMs.get(i));
+ } else {
+ newTMs.add(pickedMoves.get(pickedMoveIndex++));
+ }
+ }
+
+ this.setTMMoves(newTMs);
+ }
+
+ @Override
+ public void randomizeTMHMCompatibility(Settings settings) {
+ boolean preferSameType = settings.getTmsHmsCompatibilityMod() == Settings.TMsHMsCompatibilityMod.RANDOM_PREFER_TYPE;
+ boolean followEvolutions = settings.isTmsFollowEvolutions();
+
+ // Get current compatibility
+ // increase HM chances if required early on
+ List<Integer> requiredEarlyOn = this.getEarlyRequiredHMMoves();
+ Map<Pokemon, boolean[]> compat = this.getTMHMCompatibility();
+ List<Integer> tmHMs = new ArrayList<>(this.getTMMoves());
+ tmHMs.addAll(this.getHMMoves());
+
+ if (followEvolutions) {
+ copyUpEvolutionsHelper(pk -> randomizePokemonMoveCompatibility(
+ pk, compat.get(pk), tmHMs, requiredEarlyOn, preferSameType),
+ (evFrom, evTo, toMonIsFinalEvo) -> copyPokemonMoveCompatibilityUpEvolutions(
+ evFrom, evTo, compat.get(evFrom), compat.get(evTo), tmHMs, preferSameType
+ ), null, true);
+ }
+ else {
+ for (Map.Entry<Pokemon, boolean[]> compatEntry : compat.entrySet()) {
+ randomizePokemonMoveCompatibility(compatEntry.getKey(), compatEntry.getValue(), tmHMs,
+ requiredEarlyOn, preferSameType);
+ }
+ }
+
+ // Set the new compatibility
+ this.setTMHMCompatibility(compat);
+ }
+
+ private void randomizePokemonMoveCompatibility(Pokemon pkmn, boolean[] moveCompatibilityFlags,
+ List<Integer> moveIDs, List<Integer> prioritizedMoves,
+ boolean preferSameType) {
+ List<Move> moveData = this.getMoves();
+ for (int i = 1; i <= moveIDs.size(); i++) {
+ int move = moveIDs.get(i - 1);
+ Move mv = moveData.get(move);
+ double probability = getMoveCompatibilityProbability(
+ pkmn,
+ mv,
+ prioritizedMoves.contains(move),
+ preferSameType
+ );
+ moveCompatibilityFlags[i] = (this.random.nextDouble() < probability);
+ }
+ }
+
+ private void copyPokemonMoveCompatibilityUpEvolutions(Pokemon evFrom, Pokemon evTo, boolean[] prevCompatibilityFlags,
+ boolean[] toCompatibilityFlags, List<Integer> moveIDs,
+ boolean preferSameType) {
+ List<Move> moveData = this.getMoves();
+ for (int i = 1; i <= moveIDs.size(); i++) {
+ if (!prevCompatibilityFlags[i]) {
+ // Slight chance to gain TM/HM compatibility for a move if not learned by an earlier evolution step
+ // Without prefer same type: 25% chance
+ // With prefer same type: 10% chance, 90% chance for a type new to this evolution
+ int move = moveIDs.get(i - 1);
+ Move mv = moveData.get(move);
+ double probability = 0.25;
+ if (preferSameType) {
+ probability = 0.1;
+ if (evTo.primaryType.equals(mv.type)
+ && !evTo.primaryType.equals(evFrom.primaryType) && !evTo.primaryType.equals(evFrom.secondaryType)
+ || evTo.secondaryType != null && evTo.secondaryType.equals(mv.type)
+ && !evTo.secondaryType.equals(evFrom.secondaryType) && !evTo.secondaryType.equals(evFrom.primaryType)) {
+ probability = 0.9;
+ }
+ }
+ toCompatibilityFlags[i] = (this.random.nextDouble() < probability);
+ }
+ else {
+ toCompatibilityFlags[i] = prevCompatibilityFlags[i];
+ }
+ }
+ }
+
+ private double getMoveCompatibilityProbability(Pokemon pkmn, Move mv, boolean requiredEarlyOn,
+ boolean preferSameType) {
+ double probability = 0.5;
+ if (preferSameType) {
+ if (pkmn.primaryType.equals(mv.type)
+ || (pkmn.secondaryType != null && pkmn.secondaryType.equals(mv.type))) {
+ probability = 0.9;
+ } else if (mv.type != null && mv.type.equals(Type.NORMAL)) {
+ probability = 0.5;
+ } else {
+ probability = 0.25;
+ }
+ }
+ if (requiredEarlyOn) {
+ probability = Math.min(1.0, probability * 1.8);
+ }
+ return probability;
+ }
+
+ @Override
+ public void fullTMHMCompatibility() {
+ Map<Pokemon, boolean[]> compat = this.getTMHMCompatibility();
+ for (Map.Entry<Pokemon, boolean[]> compatEntry : compat.entrySet()) {
+ boolean[] flags = compatEntry.getValue();
+ for (int i = 1; i < flags.length; i++) {
+ flags[i] = true;
+ }
+ }
+ this.setTMHMCompatibility(compat);
+ }
+
+ @Override
+ public void ensureTMCompatSanity() {
+ // if a pokemon learns a move in its moveset
+ // and there is a TM of that move, make sure
+ // that TM can be learned.
+ Map<Pokemon, boolean[]> compat = this.getTMHMCompatibility();
+ Map<Integer, List<MoveLearnt>> movesets = this.getMovesLearnt();
+ List<Integer> tmMoves = this.getTMMoves();
+ for (Pokemon pkmn : compat.keySet()) {
+ List<MoveLearnt> moveset = movesets.get(pkmn.number);
+ boolean[] pkmnCompat = compat.get(pkmn);
+ for (MoveLearnt ml : moveset) {
+ if (tmMoves.contains(ml.move)) {
+ int tmIndex = tmMoves.indexOf(ml.move);
+ pkmnCompat[tmIndex + 1] = true;
+ }
+ }
+ }
+ this.setTMHMCompatibility(compat);
+ }
+
+ @Override
+ public void ensureTMEvolutionSanity() {
+ Map<Pokemon, boolean[]> compat = this.getTMHMCompatibility();
+ // Don't do anything with the base, just copy upwards to ensure later evolutions retain learn compatibility
+ copyUpEvolutionsHelper(pk -> {}, ((evFrom, evTo, toMonIsFinalEvo) -> {
+ boolean[] fromCompat = compat.get(evFrom);
+ boolean[] toCompat = compat.get(evTo);
+ for (int i = 1; i < toCompat.length; i++) {
+ toCompat[i] |= fromCompat[i];
+ }
+ }), null, true);
+ this.setTMHMCompatibility(compat);
+ }
+
+ @Override
+ public void fullHMCompatibility() {
+ Map<Pokemon, boolean[]> compat = this.getTMHMCompatibility();
+ int tmCount = this.getTMCount();
+ for (boolean[] flags : compat.values()) {
+ for (int i = tmCount + 1; i < flags.length; i++) {
+ flags[i] = true;
+ }
+ }
+
+ // Set the new compatibility
+ this.setTMHMCompatibility(compat);
+ }
+
+ @Override
+ public void copyTMCompatibilityToCosmeticFormes() {
+ Map<Pokemon, boolean[]> compat = this.getTMHMCompatibility();
+
+ for (Map.Entry<Pokemon, boolean[]> compatEntry : compat.entrySet()) {
+ Pokemon pkmn = compatEntry.getKey();
+ boolean[] flags = compatEntry.getValue();
+ if (pkmn.actuallyCosmetic) {
+ boolean[] baseFlags = compat.get(pkmn.baseForme);
+ for (int i = 1; i < flags.length; i++) {
+ flags[i] = baseFlags[i];
+ }
+ }
+ }
+
+ this.setTMHMCompatibility(compat);
+ }
+
+ @Override
+ public void randomizeMoveTutorMoves(Settings settings) {
+ boolean noBroken = settings.isBlockBrokenTutorMoves();
+ boolean preserveField = settings.isKeepFieldMoveTutors();
+ double goodDamagingPercentage = settings.isTutorsForceGoodDamaging() ? settings.getTutorsGoodDamagingPercent() / 100.0 : 0;
+
+ if (!this.hasMoveTutors()) {
+ return;
+ }
+
+ // Pick some random Move Tutor moves, excluding TMs.
+ List<Move> allMoves = this.getMoves();
+ List<Integer> tms = this.getTMMoves();
+ List<Integer> oldMTs = this.getMoveTutorMoves();
+ int mtCount = oldMTs.size();
+ List<Integer> hms = this.getHMMoves();
+ @SuppressWarnings("unchecked")
+ List<Integer> banned = new ArrayList<Integer>(noBroken ? this.getGameBreakingMoves() : Collections.EMPTY_LIST);
+ banned.addAll(getMovesBannedFromLevelup());
+ banned.addAll(this.getIllegalMoves());
+
+ // field moves?
+ List<Integer> fieldMoves = this.getFieldMoves();
+ int preservedFieldMoveCount = 0;
+ if (preserveField) {
+ List<Integer> banExistingField = new ArrayList<>(oldMTs);
+ banExistingField.retainAll(fieldMoves);
+ preservedFieldMoveCount = banExistingField.size();
+ banned.addAll(banExistingField);
+ }
+
+ // Determine which moves are pickable
+ List<Move> usableMoves = new ArrayList<>(allMoves);
+ usableMoves.remove(0); // remove null entry
+ Set<Move> unusableMoves = new HashSet<>();
+ Set<Move> unusableDamagingMoves = new HashSet<>();
+
+ for (Move mv : usableMoves) {
+ if (GlobalConstants.bannedRandomMoves[mv.number] || tms.contains(mv.number) || hms.contains(mv.number)
+ || banned.contains(mv.number) || GlobalConstants.zMoves.contains(mv.number)) {
+ unusableMoves.add(mv);
+ } else if (GlobalConstants.bannedForDamagingMove[mv.number]
+ || (mv.power * mv.hitCount) < GlobalConstants.MIN_DAMAGING_MOVE_POWER
+ || ((mv.power * mv.hitCount) < GlobalConstants.MIN_DAMAGING_MOVE_POWER * 2 && mv.hitratio < 90 && mv.hitratio != perfectAccuracy)) {
+ unusableDamagingMoves.add(mv);
+ }
+ }
+
+ usableMoves.removeAll(unusableMoves);
+ List<Move> usableDamagingMoves = new ArrayList<>(usableMoves);
+ usableDamagingMoves.removeAll(unusableDamagingMoves);
+
+ // pick (tmCount - preservedFieldMoveCount) moves
+ List<Integer> pickedMoves = new ArrayList<>();
+
+ // Force a certain amount of good damaging moves depending on the percentage
+ int goodDamagingLeft = (int)Math.round(goodDamagingPercentage * (mtCount - preservedFieldMoveCount));
+
+ for (int i = 0; i < mtCount - preservedFieldMoveCount; i++) {
+ Move chosenMove;
+ if (goodDamagingLeft > 0 && usableDamagingMoves.size() > 0) {
+ chosenMove = usableDamagingMoves.get(random.nextInt(usableDamagingMoves.size()));
+ } else {
+ chosenMove = usableMoves.get(random.nextInt(usableMoves.size()));
+ }
+ pickedMoves.add(chosenMove.number);
+ usableMoves.remove(chosenMove);
+ usableDamagingMoves.remove(chosenMove);
+ goodDamagingLeft--;
+ }
+
+ // shuffle the picked moves because high goodDamagingPercentage
+ // will bias them towards early numbers otherwise
+
+ Collections.shuffle(pickedMoves, random);
+
+ // finally, distribute them as tutors
+ int pickedMoveIndex = 0;
+ List<Integer> newMTs = new ArrayList<>();
+
+ for (Integer oldMT : oldMTs) {
+ if (preserveField && fieldMoves.contains(oldMT)) {
+ newMTs.add(oldMT);
+ } else {
+ newMTs.add(pickedMoves.get(pickedMoveIndex++));
+ }
+ }
+
+ this.setMoveTutorMoves(newMTs);
+ }
+
+ @Override
+ public void randomizeMoveTutorCompatibility(Settings settings) {
+ boolean preferSameType = settings.getMoveTutorsCompatibilityMod() == Settings.MoveTutorsCompatibilityMod.RANDOM_PREFER_TYPE;
+ boolean followEvolutions = settings.isTutorFollowEvolutions();
+
+ if (!this.hasMoveTutors()) {
+ return;
+ }
+ // Get current compatibility
+ Map<Pokemon, boolean[]> compat = this.getMoveTutorCompatibility();
+ List<Integer> mts = this.getMoveTutorMoves();
+
+ // Empty list
+ List<Integer> priorityTutors = new ArrayList<Integer>();
+
+ if (followEvolutions) {
+ copyUpEvolutionsHelper(pk -> randomizePokemonMoveCompatibility(
+ pk, compat.get(pk), mts, priorityTutors, preferSameType),
+ (evFrom, evTo, toMonIsFinalEvo) -> copyPokemonMoveCompatibilityUpEvolutions(
+ evFrom, evTo, compat.get(evFrom), compat.get(evTo), mts, preferSameType
+ ), null, true);
+ }
+ else {
+ for (Map.Entry<Pokemon, boolean[]> compatEntry : compat.entrySet()) {
+ randomizePokemonMoveCompatibility(compatEntry.getKey(), compatEntry.getValue(), mts, priorityTutors, preferSameType);
+ }
+ }
+
+ // Set the new compatibility
+ this.setMoveTutorCompatibility(compat);
+ }
+
+ @Override
+ public void fullMoveTutorCompatibility() {
+ if (!this.hasMoveTutors()) {
+ return;
+ }
+ Map<Pokemon, boolean[]> compat = this.getMoveTutorCompatibility();
+ for (Map.Entry<Pokemon, boolean[]> compatEntry : compat.entrySet()) {
+ boolean[] flags = compatEntry.getValue();
+ for (int i = 1; i < flags.length; i++) {
+ flags[i] = true;
+ }
+ }
+ this.setMoveTutorCompatibility(compat);
+ }
+
+ @Override
+ public void ensureMoveTutorCompatSanity() {
+ if (!this.hasMoveTutors()) {
+ return;
+ }
+ // if a pokemon learns a move in its moveset
+ // and there is a tutor of that move, make sure
+ // that tutor can be learned.
+ Map<Pokemon, boolean[]> compat = this.getMoveTutorCompatibility();
+ Map<Integer, List<MoveLearnt>> movesets = this.getMovesLearnt();
+ List<Integer> mtMoves = this.getMoveTutorMoves();
+ for (Pokemon pkmn : compat.keySet()) {
+ List<MoveLearnt> moveset = movesets.get(pkmn.number);
+ boolean[] pkmnCompat = compat.get(pkmn);
+ for (MoveLearnt ml : moveset) {
+ if (mtMoves.contains(ml.move)) {
+ int mtIndex = mtMoves.indexOf(ml.move);
+ pkmnCompat[mtIndex + 1] = true;
+ }
+ }
+ }
+ this.setMoveTutorCompatibility(compat);
+ }
+
+ @Override
+ public void ensureMoveTutorEvolutionSanity() {
+ if (!this.hasMoveTutors()) {
+ return;
+ }
+ Map<Pokemon, boolean[]> compat = this.getMoveTutorCompatibility();
+ // Don't do anything with the base, just copy upwards to ensure later evolutions retain learn compatibility
+ copyUpEvolutionsHelper(pk -> {}, ((evFrom, evTo, toMonIsFinalEvo) -> {
+ boolean[] fromCompat = compat.get(evFrom);
+ boolean[] toCompat = compat.get(evTo);
+ for (int i = 1; i < toCompat.length; i++) {
+ toCompat[i] |= fromCompat[i];
+ }
+ }), null, true);
+ this.setMoveTutorCompatibility(compat);
+ }
+
+ @Override
+ public void copyMoveTutorCompatibilityToCosmeticFormes() {
+ Map<Pokemon, boolean[]> compat = this.getMoveTutorCompatibility();
+
+ for (Map.Entry<Pokemon, boolean[]> compatEntry : compat.entrySet()) {
+ Pokemon pkmn = compatEntry.getKey();
+ boolean[] flags = compatEntry.getValue();
+ if (pkmn.actuallyCosmetic) {
+ boolean[] baseFlags = compat.get(pkmn.baseForme);
+ for (int i = 1; i < flags.length; i++) {
+ flags[i] = baseFlags[i];
+ }
+ }
+ }
+
+ this.setMoveTutorCompatibility(compat);
+ }
+
+ @SuppressWarnings("unchecked")
+ @Override
+ public void randomizeTrainerNames(Settings settings) {
+ CustomNamesSet customNames = settings.getCustomNames();
+
+ if (!this.canChangeTrainerText()) {
+ return;
+ }
+
+ // index 0 = singles, 1 = doubles
+ List<String>[] allTrainerNames = new List[] { new ArrayList<String>(), new ArrayList<String>() };
+ Map<Integer, List<String>> trainerNamesByLength[] = new Map[] { new TreeMap<Integer, List<String>>(),
+ new TreeMap<Integer, List<String>>() };
+
+ List<String> repeatedTrainerNames = Arrays.asList(new String[] { "GRUNT", "EXECUTIVE", "SHADOW", "ADMIN", "GOON", "EMPLOYEE" });
+
+ // Read name lists
+ for (String trainername : customNames.getTrainerNames()) {
+ int len = this.internalStringLength(trainername);
+ if (len <= 10) {
+ allTrainerNames[0].add(trainername);
+ if (trainerNamesByLength[0].containsKey(len)) {
+ trainerNamesByLength[0].get(len).add(trainername);
+ } else {
+ List<String> namesOfThisLength = new ArrayList<>();
+ namesOfThisLength.add(trainername);
+ trainerNamesByLength[0].put(len, namesOfThisLength);
+ }
+ }
+ }
+
+ for (String trainername : customNames.getDoublesTrainerNames()) {
+ int len = this.internalStringLength(trainername);
+ if (len <= 10) {
+ allTrainerNames[1].add(trainername);
+ if (trainerNamesByLength[1].containsKey(len)) {
+ trainerNamesByLength[1].get(len).add(trainername);
+ } else {
+ List<String> namesOfThisLength = new ArrayList<>();
+ namesOfThisLength.add(trainername);
+ trainerNamesByLength[1].put(len, namesOfThisLength);
+ }
+ }
+ }
+
+ // Get the current trainer names data
+ List<String> currentTrainerNames = this.getTrainerNames();
+ if (currentTrainerNames.size() == 0) {
+ // RBY have no trainer names
+ return;
+ }
+ TrainerNameMode mode = this.trainerNameMode();
+ int maxLength = this.maxTrainerNameLength();
+ int totalMaxLength = this.maxSumOfTrainerNameLengths();
+
+ boolean success = false;
+ int tries = 0;
+
+ // Init the translation map and new list
+ Map<String, String> translation = new HashMap<>();
+ List<String> newTrainerNames = new ArrayList<>();
+ List<Integer> tcNameLengths = this.getTCNameLengthsByTrainer();
+
+ // loop until we successfully pick names that fit
+ // should always succeed first attempt except for gen2.
+ while (!success && tries < 10000) {
+ success = true;
+ translation.clear();
+ newTrainerNames.clear();
+ int totalLength = 0;
+
+ // Start choosing
+ int tnIndex = -1;
+ for (String trainerName : currentTrainerNames) {
+ tnIndex++;
+ if (translation.containsKey(trainerName) && !repeatedTrainerNames.contains(trainerName.toUpperCase())) {
+ // use an already picked translation
+ newTrainerNames.add(translation.get(trainerName));
+ totalLength += this.internalStringLength(translation.get(trainerName));
+ } else {
+ int idx = trainerName.contains("&") ? 1 : 0;
+ List<String> pickFrom = allTrainerNames[idx];
+ int intStrLen = this.internalStringLength(trainerName);
+ if (mode == TrainerNameMode.SAME_LENGTH) {
+ pickFrom = trainerNamesByLength[idx].get(intStrLen);
+ }
+ String changeTo = trainerName;
+ int ctl = intStrLen;
+ if (pickFrom != null && pickFrom.size() > 0 && intStrLen > 0) {
+ int innerTries = 0;
+ changeTo = pickFrom.get(this.cosmeticRandom.nextInt(pickFrom.size()));
+ ctl = this.internalStringLength(changeTo);
+ while ((mode == TrainerNameMode.MAX_LENGTH && ctl > maxLength)
+ || (mode == TrainerNameMode.MAX_LENGTH_WITH_CLASS && ctl + tcNameLengths.get(tnIndex) > maxLength)) {
+ innerTries++;
+ if (innerTries == 100) {
+ changeTo = trainerName;
+ ctl = intStrLen;
+ break;
+ }
+ changeTo = pickFrom.get(this.cosmeticRandom.nextInt(pickFrom.size()));
+ ctl = this.internalStringLength(changeTo);
+ }
+ }
+ translation.put(trainerName, changeTo);
+ newTrainerNames.add(changeTo);
+ totalLength += ctl;
+ }
+
+ if (totalLength > totalMaxLength) {
+ success = false;
+ tries++;
+ break;
+ }
+ }
+ }
+
+ if (!success) {
+ throw new RandomizationException("Could not randomize trainer names in a reasonable amount of attempts."
+ + "\nPlease add some shorter names to your custom trainer names.");
+ }
+
+ // Done choosing, save
+ this.setTrainerNames(newTrainerNames);
+ }
+
+ @SuppressWarnings("unchecked")
+ @Override
+ public void randomizeTrainerClassNames(Settings settings) {
+ CustomNamesSet customNames = settings.getCustomNames();
+
+ if (!this.canChangeTrainerText()) {
+ return;
+ }
+
+ // index 0 = singles, index 1 = doubles
+ List<String> allTrainerClasses[] = new List[] { new ArrayList<String>(), new ArrayList<String>() };
+ Map<Integer, List<String>> trainerClassesByLength[] = new Map[] { new HashMap<Integer, List<String>>(),
+ new HashMap<Integer, List<String>>() };
+
+ // Read names data
+ for (String trainerClassName : customNames.getTrainerClasses()) {
+ allTrainerClasses[0].add(trainerClassName);
+ int len = this.internalStringLength(trainerClassName);
+ if (trainerClassesByLength[0].containsKey(len)) {
+ trainerClassesByLength[0].get(len).add(trainerClassName);
+ } else {
+ List<String> namesOfThisLength = new ArrayList<>();
+ namesOfThisLength.add(trainerClassName);
+ trainerClassesByLength[0].put(len, namesOfThisLength);
+ }
+ }
+
+ for (String trainerClassName : customNames.getDoublesTrainerClasses()) {
+ allTrainerClasses[1].add(trainerClassName);
+ int len = this.internalStringLength(trainerClassName);
+ if (trainerClassesByLength[1].containsKey(len)) {
+ trainerClassesByLength[1].get(len).add(trainerClassName);
+ } else {
+ List<String> namesOfThisLength = new ArrayList<>();
+ namesOfThisLength.add(trainerClassName);
+ trainerClassesByLength[1].put(len, namesOfThisLength);
+ }
+ }
+
+ // Get the current trainer names data
+ List<String> currentClassNames = this.getTrainerClassNames();
+ boolean mustBeSameLength = this.fixedTrainerClassNamesLength();
+ int maxLength = this.maxTrainerClassNameLength();
+
+ // Init the translation map and new list
+ Map<String, String> translation = new HashMap<>();
+ List<String> newClassNames = new ArrayList<>();
+
+ int numTrainerClasses = currentClassNames.size();
+ List<Integer> doublesClasses = this.getDoublesTrainerClasses();
+
+ // Start choosing
+ for (int i = 0; i < numTrainerClasses; i++) {
+ String trainerClassName = currentClassNames.get(i);
+ if (translation.containsKey(trainerClassName)) {
+ // use an already picked translation
+ newClassNames.add(translation.get(trainerClassName));
+ } else {
+ int idx = doublesClasses.contains(i) ? 1 : 0;
+ List<String> pickFrom = allTrainerClasses[idx];
+ int intStrLen = this.internalStringLength(trainerClassName);
+ if (mustBeSameLength) {
+ pickFrom = trainerClassesByLength[idx].get(intStrLen);
+ }
+ String changeTo = trainerClassName;
+ if (pickFrom != null && pickFrom.size() > 0) {
+ changeTo = pickFrom.get(this.cosmeticRandom.nextInt(pickFrom.size()));
+ while (changeTo.length() > maxLength) {
+ changeTo = pickFrom.get(this.cosmeticRandom.nextInt(pickFrom.size()));
+ }
+ }
+ translation.put(trainerClassName, changeTo);
+ newClassNames.add(changeTo);
+ }
+ }
+
+ // Done choosing, save
+ this.setTrainerClassNames(newClassNames);
+ }
+
+ @Override
+ public void randomizeWildHeldItems(Settings settings) {
+ boolean banBadItems = settings.isBanBadRandomWildPokemonHeldItems();
+
+ List<Pokemon> pokemon = allPokemonInclFormesWithoutNull();
+ ItemList possibleItems = banBadItems ? this.getNonBadItems() : this.getAllowedItems();
+ for (Pokemon pk : pokemon) {
+ if (pk.guaranteedHeldItem == -1 && pk.commonHeldItem == -1 && pk.rareHeldItem == -1
+ && pk.darkGrassHeldItem == -1) {
+ // No held items at all, abort
+ return;
+ }
+ boolean canHaveDarkGrass = pk.darkGrassHeldItem != -1;
+ if (pk.guaranteedHeldItem != -1) {
+ // Guaranteed held items are supported.
+ if (pk.guaranteedHeldItem > 0) {
+ // Currently have a guaranteed item
+ double decision = this.random.nextDouble();
+ if (decision < 0.9) {
+ // Stay as guaranteed
+ canHaveDarkGrass = false;
+ pk.guaranteedHeldItem = possibleItems.randomItem(this.random);
+ } else {
+ // Change to 25% or 55% chance
+ pk.guaranteedHeldItem = 0;
+ pk.commonHeldItem = possibleItems.randomItem(this.random);
+ pk.rareHeldItem = possibleItems.randomItem(this.random);
+ while (pk.rareHeldItem == pk.commonHeldItem) {
+ pk.rareHeldItem = possibleItems.randomItem(this.random);
+ }
+ }
+ } else {
+ // No guaranteed item atm
+ double decision = this.random.nextDouble();
+ if (decision < 0.5) {
+ // No held item at all
+ pk.commonHeldItem = 0;
+ pk.rareHeldItem = 0;
+ } else if (decision < 0.65) {
+ // Just a rare item
+ pk.commonHeldItem = 0;
+ pk.rareHeldItem = possibleItems.randomItem(this.random);
+ } else if (decision < 0.8) {
+ // Just a common item
+ pk.commonHeldItem = possibleItems.randomItem(this.random);
+ pk.rareHeldItem = 0;
+ } else if (decision < 0.95) {
+ // Both a common and rare item
+ pk.commonHeldItem = possibleItems.randomItem(this.random);
+ pk.rareHeldItem = possibleItems.randomItem(this.random);
+ while (pk.rareHeldItem == pk.commonHeldItem) {
+ pk.rareHeldItem = possibleItems.randomItem(this.random);
+ }
+ } else {
+ // Guaranteed item
+ canHaveDarkGrass = false;
+ pk.guaranteedHeldItem = possibleItems.randomItem(this.random);
+ pk.commonHeldItem = 0;
+ pk.rareHeldItem = 0;
+ }
+ }
+ } else {
+ // Code for no guaranteed items
+ double decision = this.random.nextDouble();
+ if (decision < 0.5) {
+ // No held item at all
+ pk.commonHeldItem = 0;
+ pk.rareHeldItem = 0;
+ } else if (decision < 0.65) {
+ // Just a rare item
+ pk.commonHeldItem = 0;
+ pk.rareHeldItem = possibleItems.randomItem(this.random);
+ } else if (decision < 0.8) {
+ // Just a common item
+ pk.commonHeldItem = possibleItems.randomItem(this.random);
+ pk.rareHeldItem = 0;
+ } else {
+ // Both a common and rare item
+ pk.commonHeldItem = possibleItems.randomItem(this.random);
+ pk.rareHeldItem = possibleItems.randomItem(this.random);
+ while (pk.rareHeldItem == pk.commonHeldItem) {
+ pk.rareHeldItem = possibleItems.randomItem(this.random);
+ }
+ }
+ }
+
+ if (canHaveDarkGrass) {
+ double dgDecision = this.random.nextDouble();
+ if (dgDecision < 0.5) {
+ // Yes, dark grass item
+ pk.darkGrassHeldItem = possibleItems.randomItem(this.random);
+ } else {
+ pk.darkGrassHeldItem = 0;
+ }
+ } else if (pk.darkGrassHeldItem != -1) {
+ pk.darkGrassHeldItem = 0;
+ }
+ }
+
+ }
+
+ @Override
+ public void randomizeStarterHeldItems(Settings settings) {
+ boolean banBadItems = settings.isBanBadRandomStarterHeldItems();
+
+ List<Integer> oldHeldItems = this.getStarterHeldItems();
+ List<Integer> newHeldItems = new ArrayList<>();
+ ItemList possibleItems = banBadItems ? this.getNonBadItems() : this.getAllowedItems();
+ for (int i = 0; i < oldHeldItems.size(); i++) {
+ newHeldItems.add(possibleItems.randomItem(this.random));
+ }
+ this.setStarterHeldItems(newHeldItems);
+ }
+
+ @Override
+ public void shuffleFieldItems() {
+ List<Integer> currentItems = this.getRegularFieldItems();
+ List<Integer> currentTMs = this.getCurrentFieldTMs();
+
+ Collections.shuffle(currentItems, this.random);
+ Collections.shuffle(currentTMs, this.random);
+
+ this.setRegularFieldItems(currentItems);
+ this.setFieldTMs(currentTMs);
+ }
+
+ @Override
+ public void randomizeFieldItems(Settings settings) {
+ boolean banBadItems = settings.isBanBadRandomFieldItems();
+ boolean distributeItemsControl = settings.getFieldItemsMod() == Settings.FieldItemsMod.RANDOM_EVEN;
+ boolean uniqueItems = !settings.isBalanceShopPrices();
+
+ ItemList possibleItems = banBadItems ? this.getNonBadItems().copy() : this.getAllowedItems().copy();
+ List<Integer> currentItems = this.getRegularFieldItems();
+ List<Integer> currentTMs = this.getCurrentFieldTMs();
+ List<Integer> requiredTMs = this.getRequiredFieldTMs();
+ List<Integer> uniqueNoSellItems = this.getUniqueNoSellItems();
+ // System.out.println("distributeItemsControl: "+ distributeItemsControl);
+
+ int fieldItemCount = currentItems.size();
+ int fieldTMCount = currentTMs.size();
+ int reqTMCount = requiredTMs.size();
+ int totalTMCount = this.getTMCount();
+
+ List<Integer> newItems = new ArrayList<>();
+ List<Integer> newTMs = new ArrayList<>(requiredTMs);
+
+ // List<Integer> chosenItems = new ArrayList<Integer>(); // collecting chosenItems for later process
+
+ if (distributeItemsControl) {
+ for (int i = 0; i < fieldItemCount; i++) {
+ int chosenItem = possibleItems.randomNonTM(this.random);
+ int iterNum = 0;
+ while ((this.getItemPlacementHistory(chosenItem) > this.getItemPlacementAverage()) && iterNum < 100) {
+ chosenItem = possibleItems.randomNonTM(this.random);
+ iterNum +=1;
+ }
+ newItems.add(chosenItem);
+ if (uniqueItems && uniqueNoSellItems.contains(chosenItem)) {
+ possibleItems.banSingles(chosenItem);
+ } else {
+ this.setItemPlacementHistory(chosenItem);
+ }
+ }
+ } else {
+ for (int i = 0; i < fieldItemCount; i++) {
+ int chosenItem = possibleItems.randomNonTM(this.random);
+ newItems.add(chosenItem);
+ if (uniqueItems && uniqueNoSellItems.contains(chosenItem)) {
+ possibleItems.banSingles(chosenItem);
+ }
+ }
+ }
+
+ for (int i = reqTMCount; i < fieldTMCount; i++) {
+ while (true) {
+ int tm = this.random.nextInt(totalTMCount) + 1;
+ if (!newTMs.contains(tm)) {
+ newTMs.add(tm);
+ break;
+ }
+ }
+ }
+
+
+ Collections.shuffle(newItems, this.random);
+ Collections.shuffle(newTMs, this.random);
+
+ this.setRegularFieldItems(newItems);
+ this.setFieldTMs(newTMs);
+ }
+
+ @Override
+ public void randomizeIngameTrades(Settings settings) {
+ boolean randomizeRequest = settings.getInGameTradesMod() == Settings.InGameTradesMod.RANDOMIZE_GIVEN_AND_REQUESTED;
+ boolean randomNickname = settings.isRandomizeInGameTradesNicknames();
+ boolean randomOT = settings.isRandomizeInGameTradesOTs();
+ boolean randomStats = settings.isRandomizeInGameTradesIVs();
+ boolean randomItem = settings.isRandomizeInGameTradesItems();
+ CustomNamesSet customNames = settings.getCustomNames();
+
+ checkPokemonRestrictions();
+ // Process trainer names
+ List<String> trainerNames = new ArrayList<>();
+ // Check for the file
+ if (randomOT) {
+ int maxOT = this.maxTradeOTNameLength();
+ for (String trainername : customNames.getTrainerNames()) {
+ int len = this.internalStringLength(trainername);
+ if (len <= maxOT && !trainerNames.contains(trainername)) {
+ trainerNames.add(trainername);
+ }
+ }
+ }
+
+ // Process nicknames
+ List<String> nicknames = new ArrayList<>();
+ // Check for the file
+ if (randomNickname) {
+ int maxNN = this.maxTradeNicknameLength();
+ for (String nickname : customNames.getPokemonNicknames()) {
+ int len = this.internalStringLength(nickname);
+ if (len <= maxNN && !nicknames.contains(nickname)) {
+ nicknames.add(nickname);
+ }
+ }
+ }
+
+ // get old trades
+ List<IngameTrade> trades = this.getIngameTrades();
+ List<Pokemon> usedRequests = new ArrayList<>();
+ List<Pokemon> usedGivens = new ArrayList<>();
+ List<String> usedOTs = new ArrayList<>();
+ List<String> usedNicknames = new ArrayList<>();
+ ItemList possibleItems = this.getAllowedItems();
+
+ int nickCount = nicknames.size();
+ int trnameCount = trainerNames.size();
+
+ for (IngameTrade trade : trades) {
+ // pick new given pokemon
+ Pokemon oldgiven = trade.givenPokemon;
+ Pokemon given = this.randomPokemon();
+ while (usedGivens.contains(given)) {
+ given = this.randomPokemon();
+ }
+ usedGivens.add(given);
+ trade.givenPokemon = given;
+
+ // requested pokemon?
+ if (oldgiven == trade.requestedPokemon) {
+ // preserve trades for the same pokemon
+ trade.requestedPokemon = given;
+ } else if (randomizeRequest) {
+ if (trade.requestedPokemon != null) {
+ Pokemon request = this.randomPokemon();
+ while (usedRequests.contains(request) || request == given) {
+ request = this.randomPokemon();
+ }
+ usedRequests.add(request);
+ trade.requestedPokemon = request;
+ }
+ }
+
+ // nickname?
+ if (randomNickname && nickCount > usedNicknames.size()) {
+ String nickname = nicknames.get(this.random.nextInt(nickCount));
+ while (usedNicknames.contains(nickname)) {
+ nickname = nicknames.get(this.random.nextInt(nickCount));
+ }
+ usedNicknames.add(nickname);
+ trade.nickname = nickname;
+ } else if (trade.nickname.equalsIgnoreCase(oldgiven.name)) {
+ // change the name for sanity
+ trade.nickname = trade.givenPokemon.name;
+ }
+
+ if (randomOT && trnameCount > usedOTs.size()) {
+ String ot = trainerNames.get(this.random.nextInt(trnameCount));
+ while (usedOTs.contains(ot)) {
+ ot = trainerNames.get(this.random.nextInt(trnameCount));
+ }
+ usedOTs.add(ot);
+ trade.otName = ot;
+ trade.otId = this.random.nextInt(65536);
+ }
+
+ if (randomStats) {
+ int maxIV = this.hasDVs() ? 16 : 32;
+ for (int i = 0; i < trade.ivs.length; i++) {
+ trade.ivs[i] = this.random.nextInt(maxIV);
+ }
+ }
+
+ if (randomItem) {
+ trade.item = possibleItems.randomItem(this.random);
+ }
+ }
+
+ // things that the game doesn't support should just be ignored
+ this.setIngameTrades(trades);
+ }
+
+ @Override
+ public void condenseLevelEvolutions(int maxLevel, int maxIntermediateLevel) {
+ List<Pokemon> allPokemon = this.getPokemon();
+ // search for level evolutions
+ for (Pokemon pk : allPokemon) {
+ if (pk != null) {
+ for (Evolution checkEvo : pk.evolutionsFrom) {
+ if (checkEvo.type.usesLevel()) {
+ // If evo is intermediate and too high, bring it down
+ // Else if it's just too high, bring it down
+ if (checkEvo.extraInfo > maxIntermediateLevel && checkEvo.to.evolutionsFrom.size() > 0) {
+ checkEvo.extraInfo = maxIntermediateLevel;
+ addEvoUpdateCondensed(easierEvolutionUpdates, checkEvo, false);
+ } else if (checkEvo.extraInfo > maxLevel) {
+ checkEvo.extraInfo = maxLevel;
+ addEvoUpdateCondensed(easierEvolutionUpdates, checkEvo, false);
+ }
+ }
+ if (checkEvo.type == EvolutionType.LEVEL_UPSIDE_DOWN) {
+ checkEvo.type = EvolutionType.LEVEL;
+ addEvoUpdateCondensed(easierEvolutionUpdates, checkEvo, false);
+ }
+ }
+ }
+ }
+
+ }
+
+ @Override
+ public Set<EvolutionUpdate> getImpossibleEvoUpdates() {
+ return impossibleEvolutionUpdates;
+ }
+
+ @Override
+ public Set<EvolutionUpdate> getEasierEvoUpdates() {
+ return easierEvolutionUpdates;
+ }
+
+ @Override
+ public Set<EvolutionUpdate> getTimeBasedEvoUpdates() {
+ return timeBasedEvolutionUpdates;
+ }
+
+ @Override
+ public void randomizeEvolutions(Settings settings) {
+ boolean similarStrength = settings.isEvosSimilarStrength();
+ boolean sameType = settings.isEvosSameTyping();
+ boolean limitToThreeStages = settings.isEvosMaxThreeStages();
+ boolean forceChange = settings.isEvosForceChange();
+ boolean allowAltFormes = settings.isEvosAllowAltFormes();
+ boolean banIrregularAltFormes = settings.isBanIrregularAltFormes();
+ boolean abilitiesAreRandomized = settings.getAbilitiesMod() == Settings.AbilitiesMod.RANDOMIZE;
+
+ checkPokemonRestrictions();
+ List<Pokemon> pokemonPool;
+ if (this.altFormesCanHaveDifferentEvolutions()) {
+ pokemonPool = new ArrayList<>(mainPokemonListInclFormes);
+ } else {
+ pokemonPool = new ArrayList<>(mainPokemonList);
+ }
+ List<Pokemon> actuallyCosmeticPokemonPool = new ArrayList<>();
+ int stageLimit = limitToThreeStages ? 3 : 10;
+
+ List<Pokemon> banned = this.getBannedFormesForPlayerPokemon();
+ if (!abilitiesAreRandomized) {
+ List<Pokemon> abilityDependentFormes = getAbilityDependentFormes();
+ banned.addAll(abilityDependentFormes);
+ }
+ if (banIrregularAltFormes) {
+ banned.addAll(getIrregularFormes());
+ }
+
+ for (int i = 0; i < pokemonPool.size(); i++) {
+ Pokemon pk = pokemonPool.get(i);
+ if (pk.actuallyCosmetic) {
+ pokemonPool.remove(pk);
+ i--;
+ actuallyCosmeticPokemonPool.add(pk);
+ }
+ }
+
+ // Cache old evolutions for data later
+ Map<Pokemon, List<Evolution>> originalEvos = new HashMap<>();
+ for (Pokemon pk : pokemonPool) {
+ originalEvos.put(pk, new ArrayList<>(pk.evolutionsFrom));
+ }
+
+ Set<EvolutionPair> newEvoPairs = new HashSet<>();
+ Set<EvolutionPair> oldEvoPairs = new HashSet<>();
+
+ if (forceChange) {
+ for (Pokemon pk : pokemonPool) {
+ for (Evolution ev : pk.evolutionsFrom) {
+ oldEvoPairs.add(new EvolutionPair(ev.from, ev.to));
+ if (generationOfPokemon() >= 7 && ev.from.number == Species.cosmoem) { // Special case for Cosmoem to add Lunala/Solgaleo since we remove the split evo
+ int oppositeVersionLegendary = ev.to.number == Species.solgaleo ? Species.lunala : Species.solgaleo;
+ Pokemon toPkmn = findPokemonInPoolWithSpeciesID(pokemonPool, oppositeVersionLegendary);
+ if (toPkmn != null) {
+ oldEvoPairs.add(new EvolutionPair(ev.from, toPkmn));
+ }
+ }
+ }
+ }
+ }
+
+ List<Pokemon> replacements = new ArrayList<>();
+
+ int loops = 0;
+ while (loops < 1) {
+ // Setup for this loop.
+ boolean hadError = false;
+ for (Pokemon pk : pokemonPool) {
+ pk.evolutionsFrom.clear();
+ pk.evolutionsTo.clear();
+ }
+ newEvoPairs.clear();
+
+ // Shuffle pokemon list so the results aren't overly predictable.
+ Collections.shuffle(pokemonPool, this.random);
+
+ for (Pokemon fromPK : pokemonPool) {
+ List<Evolution> oldEvos = originalEvos.get(fromPK);
+ for (Evolution ev : oldEvos) {
+ // Pick a Pokemon as replacement
+ replacements.clear();
+
+ List<Pokemon> chosenList =
+ allowAltFormes ?
+ mainPokemonListInclFormes
+ .stream()
+ .filter(pk -> !pk.actuallyCosmetic)
+ .collect(Collectors.toList()) :
+ mainPokemonList;
+ // Step 1: base filters
+ for (Pokemon pk : chosenList) {
+ // Prevent evolving into oneself (mandatory)
+ if (pk == fromPK) {
+ continue;
+ }
+
+ // Force same EXP curve (mandatory)
+ if (pk.growthCurve != fromPK.growthCurve) {
+ continue;
+ }
+
+ // Prevent evolving into banned Pokemon (mandatory)
+ if (banned.contains(pk)) {
+ continue;
+ }
+
+ EvolutionPair ep = new EvolutionPair(fromPK, pk);
+ // Prevent split evos choosing the same Pokemon
+ // (mandatory)
+ if (newEvoPairs.contains(ep)) {
+ continue;
+ }
+
+ // Prevent evolving into old thing if flagged
+ if (forceChange && oldEvoPairs.contains(ep)) {
+ continue;
+ }
+
+ // Prevent evolution that causes cycle (mandatory)
+ if (evoCycleCheck(fromPK, pk)) {
+ continue;
+ }
+
+ // Prevent evolution that exceeds stage limit
+ Evolution tempEvo = new Evolution(fromPK, pk, false, EvolutionType.NONE, 0);
+ fromPK.evolutionsFrom.add(tempEvo);
+ pk.evolutionsTo.add(tempEvo);
+ boolean exceededLimit = false;
+
+ Set<Pokemon> related = relatedPokemon(fromPK);
+
+ for (Pokemon pk2 : related) {
+ int numPreEvos = numPreEvolutions(pk2, stageLimit);
+ if (numPreEvos >= stageLimit) {
+ exceededLimit = true;
+ break;
+ } else if (numPreEvos == stageLimit - 1 && pk2.evolutionsFrom.size() == 0
+ && originalEvos.get(pk2).size() > 0) {
+ exceededLimit = true;
+ break;
+ }
+ }
+
+ fromPK.evolutionsFrom.remove(tempEvo);
+ pk.evolutionsTo.remove(tempEvo);
+
+ if (exceededLimit) {
+ continue;
+ }
+
+ // Passes everything, add as a candidate.
+ replacements.add(pk);
+ }
+
+ // If we don't have any candidates after Step 1, severe
+ // failure
+ // exit out of this loop and try again from scratch
+ if (replacements.size() == 0) {
+ hadError = true;
+ break;
+ }
+
+ // Step 2: filter by type, if needed
+ if (replacements.size() > 1 && sameType) {
+ Set<Pokemon> includeType = new HashSet<>();
+ for (Pokemon pk : replacements) {
+ // Special case for Eevee
+ if (fromPK.number == Species.eevee) {
+ if (pk.primaryType == ev.to.primaryType
+ || (pk.secondaryType != null) && pk.secondaryType == ev.to.primaryType) {
+ includeType.add(pk);
+ }
+ } else if (pk.primaryType == fromPK.primaryType
+ || (fromPK.secondaryType != null && pk.primaryType == fromPK.secondaryType)
+ || (pk.secondaryType != null && pk.secondaryType == fromPK.primaryType)
+ || (fromPK.secondaryType != null && pk.secondaryType != null && pk.secondaryType == fromPK.secondaryType)) {
+ includeType.add(pk);
+ }
+ }
+
+ if (includeType.size() != 0) {
+ replacements.retainAll(includeType);
+ }
+ }
+
+ if (!alreadyPicked.containsAll(replacements) && !similarStrength) {
+ replacements.removeAll(alreadyPicked);
+ }
+
+ // Step 3: pick - by similar strength or otherwise
+ Pokemon picked;
+
+ if (replacements.size() == 1) {
+ // Foregone conclusion.
+ picked = replacements.get(0);
+ alreadyPicked.add(picked);
+ } else if (similarStrength) {
+ picked = pickEvoPowerLvlReplacement(replacements, ev.to);
+ alreadyPicked.add(picked);
+ } else {
+ picked = replacements.get(this.random.nextInt(replacements.size()));
+ alreadyPicked.add(picked);
+ }
+
+ // Step 4: add it to the new evos pool
+ Evolution newEvo = new Evolution(fromPK, picked, ev.carryStats, ev.type, ev.extraInfo);
+ boolean checkCosmetics = true;
+ if (picked.formeNumber > 0) {
+ newEvo.forme = picked.formeNumber;
+ newEvo.formeSuffix = picked.formeSuffix;
+ checkCosmetics = false;
+ }
+ if (checkCosmetics && newEvo.to.cosmeticForms > 0) {
+ newEvo.forme = newEvo.to.getCosmeticFormNumber(this.random.nextInt(newEvo.to.cosmeticForms));
+ } else if (!checkCosmetics && picked.cosmeticForms > 0) {
+ newEvo.forme += picked.getCosmeticFormNumber(this.random.nextInt(picked.cosmeticForms));
+ }
+ if (newEvo.type == EvolutionType.LEVEL_FEMALE_ESPURR) {
+ newEvo.type = EvolutionType.LEVEL_FEMALE_ONLY;
+ }
+ fromPK.evolutionsFrom.add(newEvo);
+ picked.evolutionsTo.add(newEvo);
+ newEvoPairs.add(new EvolutionPair(fromPK, picked));
+ }
+
+ if (hadError) {
+ // No need to check the other Pokemon if we already errored
+ break;
+ }
+ }
+
+ // If no error, done and return
+ if (!hadError) {
+ for (Pokemon pk: actuallyCosmeticPokemonPool) {
+ pk.copyBaseFormeEvolutions(pk.baseForme);
+ }
+ return;
+ } else {
+ loops++;
+ }
+ }
+
+ // If we made it out of the loop, we weren't able to randomize evos.
+ throw new RandomizationException("Not able to randomize evolutions in a sane amount of retries.");
+ }
+
+ @Override
+ public void randomizeEvolutionsEveryLevel(Settings settings) {
+ boolean sameType = settings.isEvosSameTyping();
+ boolean forceChange = settings.isEvosForceChange();
+ boolean allowAltFormes = settings.isEvosAllowAltFormes();
+ boolean abilitiesAreRandomized = settings.getAbilitiesMod() == Settings.AbilitiesMod.RANDOMIZE;
+
+ checkPokemonRestrictions();
+ List<Pokemon> pokemonPool;
+ if (this.altFormesCanHaveDifferentEvolutions()) {
+ pokemonPool = new ArrayList<>(mainPokemonListInclFormes);
+ } else {
+ pokemonPool = new ArrayList<>(mainPokemonList);
+ }
+ List<Pokemon> actuallyCosmeticPokemonPool = new ArrayList<>();
+
+ List<Pokemon> banned = this.getBannedFormesForPlayerPokemon();
+ if (!abilitiesAreRandomized) {
+ List<Pokemon> abilityDependentFormes = getAbilityDependentFormes();
+ banned.addAll(abilityDependentFormes);
+ }
+
+ for (int i = 0; i < pokemonPool.size(); i++) {
+ Pokemon pk = pokemonPool.get(i);
+ if (pk.actuallyCosmetic) {
+ pokemonPool.remove(pk);
+ i--;
+ actuallyCosmeticPokemonPool.add(pk);
+ }
+ }
+
+ Set<EvolutionPair> oldEvoPairs = new HashSet<>();
+
+ if (forceChange) {
+ for (Pokemon pk : pokemonPool) {
+ for (Evolution ev : pk.evolutionsFrom) {
+ oldEvoPairs.add(new EvolutionPair(ev.from, ev.to));
+ if (generationOfPokemon() >= 7 && ev.from.number == Species.cosmoem) { // Special case for Cosmoem to add Lunala/Solgaleo since we remove the split evo
+ int oppositeVersionLegendary = ev.to.number == Species.solgaleo ? Species.lunala : Species.solgaleo;
+ Pokemon toPkmn = findPokemonInPoolWithSpeciesID(pokemonPool, oppositeVersionLegendary);
+ if (toPkmn != null) {
+ oldEvoPairs.add(new EvolutionPair(ev.from, toPkmn));
+ }
+ }
+ }
+ }
+ }
+
+ List<Pokemon> replacements = new ArrayList<>();
+
+ int loops = 0;
+ while (loops < 1) {
+ // Setup for this loop.
+ boolean hadError = false;
+ for (Pokemon pk : pokemonPool) {
+ pk.evolutionsFrom.clear();
+ pk.evolutionsTo.clear();
+ }
+
+ // Shuffle pokemon list so the results aren't overly predictable.
+ Collections.shuffle(pokemonPool, this.random);
+
+ for (Pokemon fromPK : pokemonPool) {
+ // Pick a Pokemon as replacement
+ replacements.clear();
+
+ List<Pokemon> chosenList =
+ allowAltFormes ?
+ mainPokemonListInclFormes
+ .stream()
+ .filter(pk -> !pk.actuallyCosmetic)
+ .collect(Collectors.toList()) :
+ mainPokemonList;
+ // Step 1: base filters
+ for (Pokemon pk : chosenList) {
+ // Prevent evolving into oneself (mandatory)
+ if (pk == fromPK) {
+ continue;
+ }
+
+ // Force same EXP curve (mandatory)
+ if (pk.growthCurve != fromPK.growthCurve) {
+ continue;
+ }
+
+ // Prevent evolving into banned Pokemon (mandatory)
+ if (banned.contains(pk)) {
+ continue;
+ }
+
+ // Prevent evolving into old thing if flagged
+ EvolutionPair ep = new EvolutionPair(fromPK, pk);
+ if (forceChange && oldEvoPairs.contains(ep)) {
+ continue;
+ }
+
+ // Passes everything, add as a candidate.
+ replacements.add(pk);
+ }
+
+ // If we don't have any candidates after Step 1, severe failure
+ // exit out of this loop and try again from scratch
+ if (replacements.size() == 0) {
+ hadError = true;
+ break;
+ }
+
+ // Step 2: filter by type, if needed
+ if (replacements.size() > 1 && sameType) {
+ Set<Pokemon> includeType = new HashSet<>();
+ for (Pokemon pk : replacements) {
+ if (pk.primaryType == fromPK.primaryType
+ || (fromPK.secondaryType != null && pk.primaryType == fromPK.secondaryType)
+ || (pk.secondaryType != null && pk.secondaryType == fromPK.primaryType)
+ || (pk.secondaryType != null && pk.secondaryType == fromPK.secondaryType)) {
+ includeType.add(pk);
+ }
+ }
+
+ if (includeType.size() != 0) {
+ replacements.retainAll(includeType);
+ }
+ }
+
+ // Step 3: pick - by similar strength or otherwise
+ Pokemon picked;
+
+ if (replacements.size() == 1) {
+ // Foregone conclusion.
+ picked = replacements.get(0);
+ } else {
+ picked = replacements.get(this.random.nextInt(replacements.size()));
+ }
+
+ // Step 4: create new level 1 evo and add it to the new evos pool
+ Evolution newEvo = new Evolution(fromPK, picked, false, EvolutionType.LEVEL, 1);
+ newEvo.level = 1;
+ boolean checkCosmetics = true;
+ if (picked.formeNumber > 0) {
+ newEvo.forme = picked.formeNumber;
+ newEvo.formeSuffix = picked.formeSuffix;
+ checkCosmetics = false;
+ }
+ if (checkCosmetics && newEvo.to.cosmeticForms > 0) {
+ newEvo.forme = newEvo.to.getCosmeticFormNumber(this.random.nextInt(newEvo.to.cosmeticForms));
+ } else if (!checkCosmetics && picked.cosmeticForms > 0) {
+ newEvo.forme += picked.getCosmeticFormNumber(this.random.nextInt(picked.cosmeticForms));
+ }
+ fromPK.evolutionsFrom.add(newEvo);
+ picked.evolutionsTo.add(newEvo);
+ }
+
+ // If no error, done and return
+ if (!hadError) {
+ for (Pokemon pk: actuallyCosmeticPokemonPool) {
+ pk.copyBaseFormeEvolutions(pk.baseForme);
+ }
+ return;
+ } else {
+ loops++;
+ }
+ }
+
+ // If we made it out of the loop, we weren't able to randomize evos.
+ throw new RandomizationException("Not able to randomize evolutions in a sane amount of retries.");
+ }
+
+ @Override
+ public void changeCatchRates(Settings settings) {
+ int minimumCatchRateLevel = settings.getMinimumCatchRateLevel();
+
+ int normalMin, legendaryMin;
+ switch (minimumCatchRateLevel) {
+ case 1:
+ default:
+ normalMin = 75;
+ legendaryMin = 37;
+ break;
+ case 2:
+ normalMin = 128;
+ legendaryMin = 64;
+ break;
+ case 3:
+ normalMin = 200;
+ legendaryMin = 100;
+ break;
+ case 4:
+ normalMin = legendaryMin = 255;
+ break;
+ }
+ minimumCatchRate(normalMin, legendaryMin);
+ }
+
+ @Override
+ public void shuffleShopItems() {
+ Map<Integer, Shop> currentItems = this.getShopItems();
+ if (currentItems == null) return;
+ List<Integer> itemList = new ArrayList<>();
+ for (Shop shop: currentItems.values()) {
+ itemList.addAll(shop.items);
+ }
+ Collections.shuffle(itemList, this.random);
+
+ Iterator<Integer> itemListIter = itemList.iterator();
+
+ for (Shop shop: currentItems.values()) {
+ for (int i = 0; i < shop.items.size(); i++) {
+ shop.items.remove(i);
+ shop.items.add(i, itemListIter.next());
+ }
+ }
+
+ this.setShopItems(currentItems);
+ }
+
+ // Note: If you use this on a game where the amount of randomizable shop items is greater than the amount of
+ // possible items, you will get owned by the while loop
+ @Override
+ public void randomizeShopItems(Settings settings) {
+ boolean banBadItems = settings.isBanBadRandomShopItems();
+ boolean banRegularShopItems = settings.isBanRegularShopItems();
+ boolean banOPShopItems = settings.isBanOPShopItems();
+ boolean balancePrices = settings.isBalanceShopPrices();
+ boolean placeEvolutionItems = settings.isGuaranteeEvolutionItems();
+ boolean placeXItems = settings.isGuaranteeXItems();
+
+ if (this.getShopItems() == null) return;
+ ItemList possibleItems = banBadItems ? this.getNonBadItems() : this.getAllowedItems();
+ if (banRegularShopItems) {
+ possibleItems.banSingles(this.getRegularShopItems().stream().mapToInt(Integer::intValue).toArray());
+ }
+ if (banOPShopItems) {
+ possibleItems.banSingles(this.getOPShopItems().stream().mapToInt(Integer::intValue).toArray());
+ }
+ Map<Integer, Shop> currentItems = this.getShopItems();
+
+ int shopItemCount = currentItems.values().stream().mapToInt(s -> s.items.size()).sum();
+
+ List<Integer> newItems = new ArrayList<>();
+ Map<Integer, Shop> newItemsMap = new TreeMap<>();
+ int newItem;
+ List<Integer> guaranteedItems = new ArrayList<>();
+ if (placeEvolutionItems) {
+ guaranteedItems.addAll(getEvolutionItems());
+ }
+ if (placeXItems) {
+ guaranteedItems.addAll(getXItems());
+ }
+ if (placeEvolutionItems || placeXItems) {
+ newItems.addAll(guaranteedItems);
+ shopItemCount = shopItemCount - newItems.size();
+
+ for (int i = 0; i < shopItemCount; i++) {
+ while (newItems.contains(newItem = possibleItems.randomNonTM(this.random)));
+ newItems.add(newItem);
+ }
+
+ // Guarantee main-game
+ List<Integer> mainGameShops = new ArrayList<>();
+ List<Integer> nonMainGameShops = new ArrayList<>();
+ for (int i: currentItems.keySet()) {
+ if (currentItems.get(i).isMainGame) {
+ mainGameShops.add(i);
+ } else {
+ nonMainGameShops.add(i);
+ }
+ }
+
+ // Place items in non-main-game shops; skip over guaranteed items
+ Collections.shuffle(newItems, this.random);
+ for (int i: nonMainGameShops) {
+ int j = 0;
+ List<Integer> newShopItems = new ArrayList<>();
+ Shop oldShop = currentItems.get(i);
+ for (Integer ignored: oldShop.items) {
+ Integer item = newItems.get(j);
+ while (guaranteedItems.contains(item)) {
+ j++;
+ item = newItems.get(j);
+ }
+ newShopItems.add(item);
+ newItems.remove(item);
+ }
+ Shop shop = new Shop(oldShop);
+ shop.items = newShopItems;
+ newItemsMap.put(i, shop);
+ }
+
+ // Place items in main-game shops
+ Collections.shuffle(newItems, this.random);
+ for (int i: mainGameShops) {
+ List<Integer> newShopItems = new ArrayList<>();
+ Shop oldShop = currentItems.get(i);
+ for (Integer ignored: oldShop.items) {
+ Integer item = newItems.get(0);
+ newShopItems.add(item);
+ newItems.remove(0);
+ }
+ Shop shop = new Shop(oldShop);
+ shop.items = newShopItems;
+ newItemsMap.put(i, shop);
+ }
+ } else {
+ for (int i = 0; i < shopItemCount; i++) {
+ while (newItems.contains(newItem = possibleItems.randomNonTM(this.random)));
+ newItems.add(newItem);
+ }
+
+ Iterator<Integer> newItemsIter = newItems.iterator();
+
+ for (int i: currentItems.keySet()) {
+ List<Integer> newShopItems = new ArrayList<>();
+ Shop oldShop = currentItems.get(i);
+ for (Integer ignored: oldShop.items) {
+ newShopItems.add(newItemsIter.next());
+ }
+ Shop shop = new Shop(oldShop);
+ shop.items = newShopItems;
+ newItemsMap.put(i, shop);
+ }
+ }
+
+ this.setShopItems(newItemsMap);
+ if (balancePrices) {
+ this.setShopPrices();
+ }
+ }
+
+ @Override
+ public void randomizePickupItems(Settings settings) {
+ boolean banBadItems = settings.isBanBadRandomPickupItems();
+
+ ItemList possibleItems = banBadItems ? this.getNonBadItems() : this.getAllowedItems();
+ List<PickupItem> currentItems = this.getPickupItems();
+ List<PickupItem> newItems = new ArrayList<>();
+ for (int i = 0; i < currentItems.size(); i++) {
+ int item;
+ if (this.generationOfPokemon() == 3 || this.generationOfPokemon() == 4) {
+ // Allow TMs in Gen 3/4 since they aren't infinite (and you get TMs from Pickup in the vanilla game)
+ item = possibleItems.randomItem(this.random);
+ } else {
+ item = possibleItems.randomNonTM(this.random);
+ }
+ PickupItem pickupItem = new PickupItem(item);
+ pickupItem.probabilities = Arrays.copyOf(currentItems.get(i).probabilities, currentItems.size());
+ newItems.add(pickupItem);
+ }
+
+ this.setPickupItems(newItems);
+ }
+
+ @Override
+ public void minimumCatchRate(int rateNonLegendary, int rateLegendary) {
+ List<Pokemon> pokes = getPokemonInclFormes();
+ for (Pokemon pkmn : pokes) {
+ if (pkmn == null) {
+ continue;
+ }
+ int minCatchRate = pkmn.isLegendary() ? rateLegendary : rateNonLegendary;
+ pkmn.catchRate = Math.max(pkmn.catchRate, minCatchRate);
+ }
+
+ }
+
+ @Override
+ public void standardizeEXPCurves(Settings settings) {
+ Settings.ExpCurveMod mod = settings.getExpCurveMod();
+ ExpCurve expCurve = settings.getSelectedEXPCurve();
+
+ List<Pokemon> pokes = getPokemonInclFormes();
+ switch (mod) {
+ case LEGENDARIES:
+ for (Pokemon pkmn : pokes) {
+ if (pkmn == null) {
+ continue;
+ }
+ pkmn.growthCurve = pkmn.isLegendary() ? ExpCurve.SLOW : expCurve;
+ }
+ break;
+ case STRONG_LEGENDARIES:
+ for (Pokemon pkmn : pokes) {
+ if (pkmn == null) {
+ continue;
+ }
+ pkmn.growthCurve = pkmn.isStrongLegendary() ? ExpCurve.SLOW : expCurve;
+ }
+ break;
+ case ALL:
+ for (Pokemon pkmn : pokes) {
+ if (pkmn == null) {
+ continue;
+ }
+ pkmn.growthCurve = expCurve;
+ }
+ break;
+ }
+ }
+
+ /* Private methods/structs used internally by the above methods */
+
+ private void updateMovePower(List<Move> moves, int moveNum, int power) {
+ Move mv = moves.get(moveNum);
+ if (mv.power != power) {
+ mv.power = power;
+ addMoveUpdate(moveNum, 0);
+ }
+ }
+
+ private void updateMovePP(List<Move> moves, int moveNum, int pp) {
+ Move mv = moves.get(moveNum);
+ if (mv.pp != pp) {
+ mv.pp = pp;
+ addMoveUpdate(moveNum, 1);
+ }
+ }
+
+ private void updateMoveAccuracy(List<Move> moves, int moveNum, int accuracy) {
+ Move mv = moves.get(moveNum);
+ if (Math.abs(mv.hitratio - accuracy) >= 1) {
+ mv.hitratio = accuracy;
+ addMoveUpdate(moveNum, 2);
+ }
+ }
+
+ private void updateMoveType(List<Move> moves, int moveNum, Type type) {
+ Move mv = moves.get(moveNum);
+ if (mv.type != type) {
+ mv.type = type;
+ addMoveUpdate(moveNum, 3);
+ }
+ }
+
+ private void updateMoveCategory(List<Move> moves, int moveNum, MoveCategory category) {
+ Move mv = moves.get(moveNum);
+ if (mv.category != category) {
+ mv.category = category;
+ addMoveUpdate(moveNum, 4);
+ }
+ }
+
+ private void addMoveUpdate(int moveNum, int updateType) {
+ if (!moveUpdates.containsKey(moveNum)) {
+ boolean[] updateField = new boolean[5];
+ updateField[updateType] = true;
+ moveUpdates.put(moveNum, updateField);
+ } else {
+ moveUpdates.get(moveNum)[updateType] = true;
+ }
+ }
+
+ protected Set<EvolutionUpdate> impossibleEvolutionUpdates = new TreeSet<>();
+ protected Set<EvolutionUpdate> timeBasedEvolutionUpdates = new TreeSet<>();
+ protected Set<EvolutionUpdate> easierEvolutionUpdates = new TreeSet<>();
+
+ protected void addEvoUpdateLevel(Set<EvolutionUpdate> evolutionUpdates, Evolution evo) {
+ Pokemon pkFrom = evo.from;
+ Pokemon pkTo = evo.to;
+ int level = evo.extraInfo;
+ evolutionUpdates.add(new EvolutionUpdate(pkFrom, pkTo, EvolutionType.LEVEL, String.valueOf(level),
+ false, false));
+ }
+
+ protected void addEvoUpdateStone(Set<EvolutionUpdate> evolutionUpdates, Evolution evo, String item) {
+ Pokemon pkFrom = evo.from;
+ Pokemon pkTo = evo.to;
+ evolutionUpdates.add(new EvolutionUpdate(pkFrom, pkTo, EvolutionType.STONE, item,
+ false, false));
+ }
+
+ protected void addEvoUpdateHappiness(Set<EvolutionUpdate> evolutionUpdates, Evolution evo) {
+ Pokemon pkFrom = evo.from;
+ Pokemon pkTo = evo.to;
+ evolutionUpdates.add(new EvolutionUpdate(pkFrom, pkTo, EvolutionType.HAPPINESS, "",
+ false, false));
+ }
+
+ protected void addEvoUpdateHeldItem(Set<EvolutionUpdate> evolutionUpdates, Evolution evo, String item) {
+ Pokemon pkFrom = evo.from;
+ Pokemon pkTo = evo.to;
+ evolutionUpdates.add(new EvolutionUpdate(pkFrom, pkTo, EvolutionType.LEVEL_ITEM_DAY, item,
+ false, false));
+ }
+
+ protected void addEvoUpdateParty(Set<EvolutionUpdate> evolutionUpdates, Evolution evo, String otherPk) {
+ Pokemon pkFrom = evo.from;
+ Pokemon pkTo = evo.to;
+ evolutionUpdates.add(new EvolutionUpdate(pkFrom, pkTo, EvolutionType.LEVEL_WITH_OTHER, otherPk,
+ false, false));
+ }
+
+ protected void addEvoUpdateCondensed(Set<EvolutionUpdate> evolutionUpdates, Evolution evo, boolean additional) {
+ Pokemon pkFrom = evo.from;
+ Pokemon pkTo = evo.to;
+ int level = evo.extraInfo;
+ evolutionUpdates.add(new EvolutionUpdate(pkFrom, pkTo, EvolutionType.LEVEL, String.valueOf(level),
+ true, additional));
+ }
+
+ private Pokemon pickEvoPowerLvlReplacement(List<Pokemon> pokemonPool, Pokemon current) {
+ // start with within 10% and add 5% either direction till we find
+ // something
+ int currentBST = current.bstForPowerLevels();
+ int minTarget = currentBST - currentBST / 10;
+ int maxTarget = currentBST + currentBST / 10;
+ List<Pokemon> canPick = new ArrayList<>();
+ List<Pokemon> emergencyPick = new ArrayList<>();
+ int expandRounds = 0;
+ while (canPick.isEmpty() || (canPick.size() < 3 && expandRounds < 3)) {
+ for (Pokemon pk : pokemonPool) {
+ if (pk.bstForPowerLevels() >= minTarget && pk.bstForPowerLevels() <= maxTarget && !canPick.contains(pk) && !emergencyPick.contains(pk)) {
+ if (alreadyPicked.contains(pk)) {
+ emergencyPick.add(pk);
+ } else {
+ canPick.add(pk);
+ }
+ }
+ }
+ if (expandRounds >= 2 && canPick.isEmpty()) {
+ canPick.addAll(emergencyPick);
+ }
+ minTarget -= currentBST / 20;
+ maxTarget += currentBST / 20;
+ expandRounds++;
+ }
+ return canPick.get(this.random.nextInt(canPick.size()));
+ }
+
+ // Note that this is slow and somewhat hacky.
+ private Pokemon findPokemonInPoolWithSpeciesID(List<Pokemon> pokemonPool, int speciesID) {
+ for (int i = 0; i < pokemonPool.size(); i++) {
+ if (pokemonPool.get(i).number == speciesID) {
+ return pokemonPool.get(i);
+ }
+ }
+ return null;
+ }
+
+ private List<Pokemon> getEvolutionaryRelatives(Pokemon pk) {
+ List<Pokemon> evolutionaryRelatives = new ArrayList<>();
+ for (Evolution ev : pk.evolutionsFrom) {
+ if (!evolutionaryRelatives.contains(ev.to)) {
+ Pokemon evo = ev.to;
+ evolutionaryRelatives.add(evo);
+ Queue<Evolution> evolutionsList = new LinkedList<>();
+ evolutionsList.addAll(evo.evolutionsFrom);
+ while (evolutionsList.size() > 0) {
+ evo = evolutionsList.remove().to;
+ if (!evolutionaryRelatives.contains(evo)) {
+ evolutionaryRelatives.add(evo);
+ evolutionsList.addAll(evo.evolutionsFrom);
+ }
+ }
+ }
+ }
+
+ for (Evolution ev : pk.evolutionsTo) {
+ if (!evolutionaryRelatives.contains(ev.from)) {
+ Pokemon preEvo = ev.from;
+ evolutionaryRelatives.add(preEvo);
+
+ // At this point, preEvo is basically the "parent" of pk. Run
+ // getEvolutionaryRelatives on preEvo in order to get pk's
+ // "sibling" evolutions too. For example, if pk is Espeon, then
+ // preEvo here will be Eevee, and this will add all the other
+ // eeveelutions to the relatives list.
+ List<Pokemon> relativesForPreEvo = getEvolutionaryRelatives(preEvo);
+ for (Pokemon preEvoRelative : relativesForPreEvo) {
+ if (!evolutionaryRelatives.contains(preEvoRelative)) {
+ evolutionaryRelatives.add(preEvoRelative);
+ }
+ }
+
+ while (preEvo.evolutionsTo.size() > 0) {
+ preEvo = preEvo.evolutionsTo.get(0).from;
+ if (!evolutionaryRelatives.contains(preEvo)) {
+ evolutionaryRelatives.add(preEvo);
+
+ // Similar to above, get the "sibling" evolutions here too.
+ relativesForPreEvo = getEvolutionaryRelatives(preEvo);
+ for (Pokemon preEvoRelative : relativesForPreEvo) {
+ if (!evolutionaryRelatives.contains(preEvoRelative)) {
+ evolutionaryRelatives.add(preEvoRelative);
+ }
+ }
+ }
+ }
+ }
+ }
+
+ return evolutionaryRelatives;
+ }
+
+ private static class EvolutionPair {
+ private Pokemon from;
+ private Pokemon to;
+
+ EvolutionPair(Pokemon from, Pokemon to) {
+ this.from = from;
+ this.to = to;
+ }
+
+ @Override
+ public int hashCode() {
+ final int prime = 31;
+ int result = 1;
+ result = prime * result + ((from == null) ? 0 : from.hashCode());
+ result = prime * result + ((to == null) ? 0 : to.hashCode());
+ return result;
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (this == obj)
+ return true;
+ if (obj == null)
+ return false;
+ if (getClass() != obj.getClass())
+ return false;
+ EvolutionPair other = (EvolutionPair) obj;
+ if (from == null) {
+ if (other.from != null)
+ return false;
+ } else if (!from.equals(other.from))
+ return false;
+ if (to == null) {
+ return other.to == null;
+ } else return to.equals(other.to);
+ }
+ }
+
+ /**
+ * Check whether adding an evolution from one Pokemon to another will cause
+ * an evolution cycle.
+ *
+ * @param from Pokemon that is evolving
+ * @param to Pokemon to evolve to
+ * @return True if there is an evolution cycle, else false
+ */
+ private boolean evoCycleCheck(Pokemon from, Pokemon to) {
+ Evolution tempEvo = new Evolution(from, to, false, EvolutionType.NONE, 0);
+ from.evolutionsFrom.add(tempEvo);
+ Set<Pokemon> visited = new HashSet<>();
+ Set<Pokemon> recStack = new HashSet<>();
+ boolean recur = isCyclic(from, visited, recStack);
+ from.evolutionsFrom.remove(tempEvo);
+ return recur;
+ }
+
+ private boolean isCyclic(Pokemon pk, Set<Pokemon> visited, Set<Pokemon> recStack) {
+ if (!visited.contains(pk)) {
+ visited.add(pk);
+ recStack.add(pk);
+ for (Evolution ev : pk.evolutionsFrom) {
+ if (!visited.contains(ev.to) && isCyclic(ev.to, visited, recStack)) {
+ return true;
+ } else if (recStack.contains(ev.to)) {
+ return true;
+ }
+ }
+ }
+ recStack.remove(pk);
+ return false;
+ }
+
+ private interface BasePokemonAction {
+ void applyTo(Pokemon pk);
+ }
+
+ private interface EvolvedPokemonAction {
+ void applyTo(Pokemon evFrom, Pokemon evTo, boolean toMonIsFinalEvo);
+ }
+
+ private interface CosmeticFormAction {
+ void applyTo(Pokemon pk, Pokemon baseForme);
+ }
+
+ /**
+ * Universal implementation for things that have "copy X up evolutions"
+ * support.
+ * @param bpAction
+ * Method to run on all base or no-copy Pokemon
+ * @param epAction
+ * Method to run on all evolved Pokemon with a linear chain of
+ * @param copySplitEvos
+ * If true, treat split evolutions the same way as base Pokemon
+ */
+ private void copyUpEvolutionsHelper(BasePokemonAction bpAction, EvolvedPokemonAction epAction,
+ EvolvedPokemonAction splitAction, boolean copySplitEvos) {
+ List<Pokemon> allPokes = this.getPokemonInclFormes();
+ for (Pokemon pk : allPokes) {
+ if (pk != null) {
+ pk.temporaryFlag = false;
+ }
+ }
+
+ // Get evolution data.
+ Set<Pokemon> basicPokes = RomFunctions.getBasicPokemon(this);
+ Set<Pokemon> splitEvos = RomFunctions.getSplitEvolutions(this);
+ Set<Pokemon> middleEvos = RomFunctions.getMiddleEvolutions(this, copySplitEvos);
+
+ for (Pokemon pk : basicPokes) {
+ bpAction.applyTo(pk);
+ pk.temporaryFlag = true;
+ }
+
+ if (!copySplitEvos) {
+ for (Pokemon pk : splitEvos) {
+ bpAction.applyTo(pk);
+ pk.temporaryFlag = true;
+ }
+ }
+
+ // go "up" evolutions looking for pre-evos to do first
+ for (Pokemon pk : allPokes) {
+ if (pk != null && !pk.temporaryFlag) {
+
+ // Non-randomized pokes at this point must have
+ // a linear chain of single evolutions down to
+ // a randomized poke.
+ Stack<Evolution> currentStack = new Stack<>();
+ Evolution ev = pk.evolutionsTo.get(0);
+ while (!ev.from.temporaryFlag) {
+ currentStack.push(ev);
+ ev = ev.from.evolutionsTo.get(0);
+ }
+
+ // Now "ev" is set to an evolution from a Pokemon that has had
+ // the base action done on it to one that hasn't.
+ // Do the evolution action for everything left on the stack.
+
+ if (copySplitEvos && splitAction != null && splitEvos.contains(ev.to)) {
+ splitAction.applyTo(ev.from, ev.to, !middleEvos.contains(ev.to));
+ } else {
+ epAction.applyTo(ev.from, ev.to, !middleEvos.contains(ev.to));
+ }
+ ev.to.temporaryFlag = true;
+ while (!currentStack.isEmpty()) {
+ ev = currentStack.pop();
+ if (copySplitEvos && splitAction != null && splitEvos.contains(pk)) {
+ splitAction.applyTo(ev.from, ev.to, !middleEvos.contains(ev.to));
+ } else {
+ epAction.applyTo(ev.from, ev.to, !middleEvos.contains(ev.to));
+ }
+ ev.to.temporaryFlag = true;
+ }
+
+ }
+ }
+ }
+
+ private void copyUpEvolutionsHelper(BasePokemonAction bpAction, EvolvedPokemonAction epAction) {
+ copyUpEvolutionsHelper(bpAction, epAction, null, false);
+ }
+
+ private boolean checkForUnusedMove(List<Move> potentialList, List<Integer> alreadyUsed) {
+ for (Move mv : potentialList) {
+ if (!alreadyUsed.contains(mv.number)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ private List<Pokemon> pokemonOfType(Type type, boolean noLegendaries) {
+ List<Pokemon> typedPokes = new ArrayList<>();
+ for (Pokemon pk : mainPokemonList) {
+ if (pk != null && (!noLegendaries || !pk.isLegendary()) && !pk.actuallyCosmetic) {
+ if (pk.primaryType == type || pk.secondaryType == type) {
+ typedPokes.add(pk);
+ }
+ }
+ }
+ return typedPokes;
+ }
+
+ private List<Pokemon> pokemonOfTypeInclFormes(Type type, boolean noLegendaries) {
+ List<Pokemon> typedPokes = new ArrayList<>();
+ for (Pokemon pk : mainPokemonListInclFormes) {
+ if (pk != null && !pk.actuallyCosmetic && (!noLegendaries || !pk.isLegendary())) {
+ if (pk.primaryType == type || pk.secondaryType == type) {
+ typedPokes.add(pk);
+ }
+ }
+ }
+ return typedPokes;
+ }
+
+ private List<Pokemon> allPokemonWithoutNull() {
+ List<Pokemon> allPokes = new ArrayList<>(this.getPokemon());
+ allPokes.remove(0);
+ return allPokes;
+ }
+
+ private List<Pokemon> allPokemonInclFormesWithoutNull() {
+ List<Pokemon> allPokes = new ArrayList<>(this.getPokemonInclFormes());
+ allPokes.remove(0);
+ return allPokes;
+ }
+
+ private Set<Pokemon> pokemonInArea(EncounterSet area) {
+ Set<Pokemon> inArea = new TreeSet<>();
+ for (Encounter enc : area.encounters) {
+ inArea.add(enc.pokemon);
+ }
+ return inArea;
+ }
+
+ private Map<Type, Integer> typeWeightings;
+ private int totalTypeWeighting;
+
+ private Type pickType(boolean weightByFrequency, boolean noLegendaries, boolean allowAltFormes) {
+ if (totalTypeWeighting == 0) {
+ // Determine weightings
+ for (Type t : Type.values()) {
+ if (typeInGame(t)) {
+ List<Pokemon> pokemonOfType = allowAltFormes ? pokemonOfTypeInclFormes(t, noLegendaries) :
+ pokemonOfType(t, noLegendaries);
+ int pkWithTyping = pokemonOfType.size();
+ typeWeightings.put(t, pkWithTyping);
+ totalTypeWeighting += pkWithTyping;
+ }
+ }
+ }
+
+ if (weightByFrequency) {
+ int typePick = this.random.nextInt(totalTypeWeighting);
+ int typePos = 0;
+ for (Type t : typeWeightings.keySet()) {
+ int weight = typeWeightings.get(t);
+ if (typePos + weight > typePick) {
+ return t;
+ }
+ typePos += weight;
+ }
+ return null;
+ } else {
+ return randomType();
+ }
+ }
+
+ private void rivalCarriesStarterUpdate(List<Trainer> currentTrainers, String prefix, int pokemonOffset) {
+ // Find the highest rival battle #
+ int highestRivalNum = 0;
+ for (Trainer t : currentTrainers) {
+ if (t.tag != null && t.tag.startsWith(prefix)) {
+ highestRivalNum = Math.max(highestRivalNum,
+ Integer.parseInt(t.tag.substring(prefix.length(), t.tag.indexOf('-'))));
+ }
+ }
+
+ if (highestRivalNum == 0) {
+ // This rival type not used in this game
+ return;
+ }
+
+ // Get the starters
+ // us 0 1 2 => them 0+n 1+n 2+n
+ List<Pokemon> starters = this.getStarters();
+
+ // Yellow needs its own case, unfortunately.
+ if (isYellow()) {
+ // The rival's starter is index 1
+ Pokemon rivalStarter = starters.get(1);
+ int timesEvolves = numEvolutions(rivalStarter, 2);
+ // Yellow does not have abilities
+ int abilitySlot = 0;
+ // Apply evolutions as appropriate
+ if (timesEvolves == 0) {
+ for (int j = 1; j <= 3; j++) {
+ changeStarterWithTag(currentTrainers, prefix + j + "-0", rivalStarter, abilitySlot);
+ }
+ for (int j = 4; j <= 7; j++) {
+ for (int i = 0; i < 3; i++) {
+ changeStarterWithTag(currentTrainers, prefix + j + "-" + i, rivalStarter, abilitySlot);
+ }
+ }
+ } else if (timesEvolves == 1) {
+ for (int j = 1; j <= 3; j++) {
+ changeStarterWithTag(currentTrainers, prefix + j + "-0", rivalStarter, abilitySlot);
+ }
+ rivalStarter = pickRandomEvolutionOf(rivalStarter, false);
+ for (int j = 4; j <= 7; j++) {
+ for (int i = 0; i < 3; i++) {
+ changeStarterWithTag(currentTrainers, prefix + j + "-" + i, rivalStarter, abilitySlot);
+ }
+ }
+ } else if (timesEvolves == 2) {
+ for (int j = 1; j <= 2; j++) {
+ changeStarterWithTag(currentTrainers, prefix + j + "-" + 0, rivalStarter, abilitySlot);
+ }
+ rivalStarter = pickRandomEvolutionOf(rivalStarter, true);
+ changeStarterWithTag(currentTrainers, prefix + "3-0", rivalStarter, abilitySlot);
+ for (int i = 0; i < 3; i++) {
+ changeStarterWithTag(currentTrainers, prefix + "4-" + i, rivalStarter, abilitySlot);
+ }
+ rivalStarter = pickRandomEvolutionOf(rivalStarter, false);
+ for (int j = 5; j <= 7; j++) {
+ for (int i = 0; i < 3; i++) {
+ changeStarterWithTag(currentTrainers, prefix + j + "-" + i, rivalStarter, abilitySlot);
+ }
+ }
+ }
+ } else {
+ // Replace each starter as appropriate
+ // Use level to determine when to evolve, not number anymore
+ for (int i = 0; i < 3; i++) {
+ // Rival's starters are pokemonOffset over from each of ours
+ int starterToUse = (i + pokemonOffset) % 3;
+ Pokemon thisStarter = starters.get(starterToUse);
+ int timesEvolves = numEvolutions(thisStarter, 2);
+ int abilitySlot = getRandomAbilitySlot(thisStarter);
+ while (abilitySlot == 3) {
+ // Since starters never have hidden abilities, the rival's starter shouldn't either
+ abilitySlot = getRandomAbilitySlot(thisStarter);
+ }
+ // If a fully evolved pokemon, use throughout
+ // Otherwise split by evolutions as appropriate
+ if (timesEvolves == 0) {
+ for (int j = 1; j <= highestRivalNum; j++) {
+ changeStarterWithTag(currentTrainers, prefix + j + "-" + i, thisStarter, abilitySlot);
+ }
+ } else if (timesEvolves == 1) {
+ int j = 1;
+ for (; j <= highestRivalNum / 2; j++) {
+ if (getLevelOfStarter(currentTrainers, prefix + j + "-" + i) >= 30) {
+ break;
+ }
+ changeStarterWithTag(currentTrainers, prefix + j + "-" + i, thisStarter, abilitySlot);
+ }
+ thisStarter = pickRandomEvolutionOf(thisStarter, false);
+ int evolvedAbilitySlot = getValidAbilitySlotFromOriginal(thisStarter, abilitySlot);
+ for (; j <= highestRivalNum; j++) {
+ changeStarterWithTag(currentTrainers, prefix + j + "-" + i, thisStarter, evolvedAbilitySlot);
+ }
+ } else if (timesEvolves == 2) {
+ int j = 1;
+ for (; j <= highestRivalNum; j++) {
+ if (getLevelOfStarter(currentTrainers, prefix + j + "-" + i) >= 16) {
+ break;
+ }
+ changeStarterWithTag(currentTrainers, prefix + j + "-" + i, thisStarter, abilitySlot);
+ }
+ thisStarter = pickRandomEvolutionOf(thisStarter, true);
+ int evolvedAbilitySlot = getValidAbilitySlotFromOriginal(thisStarter, abilitySlot);
+ for (; j <= highestRivalNum; j++) {
+ if (getLevelOfStarter(currentTrainers, prefix + j + "-" + i) >= 36) {
+ break;
+ }
+ changeStarterWithTag(currentTrainers, prefix + j + "-" + i, thisStarter, evolvedAbilitySlot);
+ }
+ thisStarter = pickRandomEvolutionOf(thisStarter, false);
+ evolvedAbilitySlot = getValidAbilitySlotFromOriginal(thisStarter, abilitySlot);
+ for (; j <= highestRivalNum; j++) {
+ changeStarterWithTag(currentTrainers, prefix + j + "-" + i, thisStarter, evolvedAbilitySlot);
+ }
+ }
+ }
+ }
+
+ }
+
+ private Pokemon pickRandomEvolutionOf(Pokemon base, boolean mustEvolveItself) {
+ // Used for "rival carries starter"
+ // Pick a random evolution of base Pokemon, subject to
+ // "must evolve itself" if appropriate.
+ List<Pokemon> candidates = new ArrayList<>();
+ for (Evolution ev : base.evolutionsFrom) {
+ if (!mustEvolveItself || ev.to.evolutionsFrom.size() > 0) {
+ candidates.add(ev.to);
+ }
+ }
+
+ if (candidates.size() == 0) {
+ throw new RandomizationException("Random evolution called on a Pokemon without any usable evolutions.");
+ }
+
+ return candidates.get(random.nextInt(candidates.size()));
+ }
+
+ private int getLevelOfStarter(List<Trainer> currentTrainers, String tag) {
+ for (Trainer t : currentTrainers) {
+ if (t.tag != null && t.tag.equals(tag)) {
+ // Bingo, get highest level
+ // last pokemon is given priority +2 but equal priority
+ // = first pokemon wins, so its effectively +1
+ // If it's tagged the same we can assume it's the same team
+ // just the opposite gender or something like that...
+ // So no need to check other trainers with same tag.
+ int highestLevel = t.pokemon.get(0).level;
+ int trainerPkmnCount = t.pokemon.size();
+ for (int i = 1; i < trainerPkmnCount; i++) {
+ int levelBonus = (i == trainerPkmnCount - 1) ? 2 : 0;
+ if (t.pokemon.get(i).level + levelBonus > highestLevel) {
+ highestLevel = t.pokemon.get(i).level;
+ }
+ }
+ return highestLevel;
+ }
+ }
+ return 0;
+ }
+
+ private void changeStarterWithTag(List<Trainer> currentTrainers, String tag, Pokemon starter, int abilitySlot) {
+ for (Trainer t : currentTrainers) {
+ if (t.tag != null && t.tag.equals(tag)) {
+
+ // Bingo
+ TrainerPokemon bestPoke = t.pokemon.get(0);
+
+ if (t.forceStarterPosition >= 0) {
+ bestPoke = t.pokemon.get(t.forceStarterPosition);
+ } else {
+ // Change the highest level pokemon, not the last.
+ // BUT: last gets +2 lvl priority (effectively +1)
+ // same as above, equal priority = earlier wins
+ int trainerPkmnCount = t.pokemon.size();
+ for (int i = 1; i < trainerPkmnCount; i++) {
+ int levelBonus = (i == trainerPkmnCount - 1) ? 2 : 0;
+ if (t.pokemon.get(i).level + levelBonus > bestPoke.level) {
+ bestPoke = t.pokemon.get(i);
+ }
+ }
+ }
+ bestPoke.pokemon = starter;
+ bestPoke.absolutePokeNumber = starter.number;
+ setFormeForTrainerPokemon(bestPoke,starter);
+ bestPoke.resetMoves = true;
+ bestPoke.abilitySlot = abilitySlot;
+ }
+ }
+
+ }
+
+ // Return the max depth of pre-evolutions a Pokemon has
+ private int numPreEvolutions(Pokemon pk, int maxInterested) {
+ return numPreEvolutions(pk, 0, maxInterested);
+ }
+
+ private int numPreEvolutions(Pokemon pk, int depth, int maxInterested) {
+ if (pk.evolutionsTo.size() == 0) {
+ return 0;
+ } else {
+ if (depth == maxInterested - 1) {
+ return 1;
+ } else {
+ int maxPreEvos = 0;
+ for (Evolution ev : pk.evolutionsTo) {
+ maxPreEvos = Math.max(maxPreEvos, numPreEvolutions(ev.from, depth + 1, maxInterested) + 1);
+ }
+ return maxPreEvos;
+ }
+ }
+ }
+
+ private int numEvolutions(Pokemon pk, int maxInterested) {
+ return numEvolutions(pk, 0, maxInterested);
+ }
+
+ private int numEvolutions(Pokemon pk, int depth, int maxInterested) {
+ if (pk.evolutionsFrom.size() == 0) {
+ return 0;
+ } else {
+ if (depth == maxInterested - 1) {
+ return 1;
+ } else {
+ int maxEvos = 0;
+ for (Evolution ev : pk.evolutionsFrom) {
+ maxEvos = Math.max(maxEvos, numEvolutions(ev.to, depth + 1, maxInterested) + 1);
+ }
+ return maxEvos;
+ }
+ }
+ }
+
+ private Pokemon fullyEvolve(Pokemon pokemon) {
+ Set<Pokemon> seenMons = new HashSet<>();
+ seenMons.add(pokemon);
+
+ while (true) {
+ if (pokemon.evolutionsFrom.size() == 0) {
+ // fully evolved
+ break;
+ }
+
+ // check for cyclic evolutions from what we've already seen
+ boolean cyclic = false;
+ for (Evolution ev : pokemon.evolutionsFrom) {
+ if (seenMons.contains(ev.to)) {
+ // cyclic evolution detected - bail now
+ cyclic = true;
+ break;
+ }
+ }
+
+ if (cyclic) {
+ break;
+ }
+
+ // pick a random evolution to continue from
+ pokemon = pokemon.evolutionsFrom.get(random.nextInt(pokemon.evolutionsFrom.size())).to;
+ seenMons.add(pokemon);
+ }
+
+ return pokemon;
+ }
+
+ private Set<Pokemon> relatedPokemon(Pokemon original) {
+ Set<Pokemon> results = new HashSet<>();
+ results.add(original);
+ Queue<Pokemon> toCheck = new LinkedList<>();
+ toCheck.add(original);
+ while (!toCheck.isEmpty()) {
+ Pokemon check = toCheck.poll();
+ for (Evolution ev : check.evolutionsFrom) {
+ if (!results.contains(ev.to)) {
+ results.add(ev.to);
+ toCheck.add(ev.to);
+ }
+ }
+ for (Evolution ev : check.evolutionsTo) {
+ if (!results.contains(ev.from)) {
+ results.add(ev.from);
+ toCheck.add(ev.from);
+ }
+ }
+ }
+ return results;
+ }
+
+ private Map<Type, List<Pokemon>> cachedReplacementLists;
+ private List<Pokemon> cachedAllList;
+ private List<Pokemon> bannedList = new ArrayList<>();
+ private List<Pokemon> usedAsUniqueList = new ArrayList<>();
+
+
+ private Pokemon pickTrainerPokeReplacement(Pokemon current, boolean usePowerLevels, Type type,
+ boolean noLegendaries, boolean wonderGuardAllowed,
+ boolean usePlacementHistory, boolean swapMegaEvos,
+ boolean abilitiesAreRandomized, boolean allowAltFormes,
+ boolean banIrregularAltFormes) {
+ List<Pokemon> pickFrom;
+ List<Pokemon> withoutBannedPokemon;
+
+ if (swapMegaEvos) {
+ pickFrom = megaEvolutionsList
+ .stream()
+ .filter(mega -> mega.method == 1)
+ .map(mega -> mega.from)
+ .distinct()
+ .collect(Collectors.toList());
+ } else {
+ pickFrom = cachedAllList;
+ }
+
+ if (usePlacementHistory) {
+ // "Distributed" settings
+ double placementAverage = getPlacementAverage();
+ pickFrom = pickFrom
+ .stream()
+ .filter(pk -> getPlacementHistory(pk) < placementAverage * 2)
+ .collect(Collectors.toList());
+ if (pickFrom.isEmpty()) {
+ pickFrom = cachedAllList;
+ }
+ } else if (type != null && cachedReplacementLists != null) {
+ // "Type Themed" settings
+ if (!cachedReplacementLists.containsKey(type)) {
+ List<Pokemon> pokemonOfType = allowAltFormes ? pokemonOfTypeInclFormes(type, noLegendaries) :
+ pokemonOfType(type, noLegendaries);
+ pokemonOfType.removeAll(this.getBannedFormesForPlayerPokemon());
+ if (!abilitiesAreRandomized) {
+ List<Pokemon> abilityDependentFormes = getAbilityDependentFormes();
+ pokemonOfType.removeAll(abilityDependentFormes);
+ }
+ if (banIrregularAltFormes) {
+ pokemonOfType.removeAll(getIrregularFormes());
+ }
+ cachedReplacementLists.put(type, pokemonOfType);
+ }
+ if (swapMegaEvos) {
+ pickFrom = cachedReplacementLists.get(type)
+ .stream()
+ .filter(pickFrom::contains)
+ .collect(Collectors.toList());
+ if (pickFrom.isEmpty()) {
+ pickFrom = cachedReplacementLists.get(type);
+ }
+ } else {
+ pickFrom = cachedReplacementLists.get(type);
+ }
+ }
+
+ withoutBannedPokemon = pickFrom.stream().filter(pk -> !bannedList.contains(pk)).collect(Collectors.toList());
+ if (!withoutBannedPokemon.isEmpty()) {
+ pickFrom = withoutBannedPokemon;
+ }
+
+ if (usePowerLevels) {
+ // start with within 10% and add 5% either direction till we find
+ // something
+ int currentBST = current.bstForPowerLevels();
+ int minTarget = currentBST - currentBST / 10;
+ int maxTarget = currentBST + currentBST / 10;
+ List<Pokemon> canPick = new ArrayList<>();
+ int expandRounds = 0;
+ while (canPick.isEmpty() || (canPick.size() < 3 && expandRounds < 2)) {
+ for (Pokemon pk : pickFrom) {
+ if (pk.bstForPowerLevels() >= minTarget
+ && pk.bstForPowerLevels() <= maxTarget
+ && (wonderGuardAllowed || (pk.ability1 != Abilities.wonderGuard
+ && pk.ability2 != Abilities.wonderGuard && pk.ability3 != Abilities.wonderGuard))) {
+ canPick.add(pk);
+ }
+ }
+ minTarget -= currentBST / 20;
+ maxTarget += currentBST / 20;
+ expandRounds++;
+ }
+ // If usePlacementHistory is True, then we need to do some
+ // extra checking to make sure the randomly chosen pokemon
+ // is actually below the current average placement
+ // if not, re-roll
+
+ Pokemon chosenPokemon = canPick.get(this.random.nextInt(canPick.size()));
+ if (usePlacementHistory) {
+ double placementAverage = getPlacementAverage();
+ List<Pokemon> filteredPickList = canPick
+ .stream()
+ .filter(pk -> getPlacementHistory(pk) < placementAverage)
+ .collect(Collectors.toList());
+ if (filteredPickList.isEmpty()) {
+ filteredPickList = canPick;
+ }
+ chosenPokemon = filteredPickList.get(this.random.nextInt(filteredPickList.size()));
+ }
+ return chosenPokemon;
+ } else {
+ if (wonderGuardAllowed) {
+ return pickFrom.get(this.random.nextInt(pickFrom.size()));
+ } else {
+ Pokemon pk = pickFrom.get(this.random.nextInt(pickFrom.size()));
+ while (pk.ability1 == Abilities.wonderGuard
+ || pk.ability2 == Abilities.wonderGuard
+ || pk.ability3 == Abilities.wonderGuard) {
+ pk = pickFrom.get(this.random.nextInt(pickFrom.size()));
+ }
+ return pk;
+ }
+ }
+ }
+
+ private Pokemon pickWildPowerLvlReplacement(List<Pokemon> pokemonPool, Pokemon current, boolean banSamePokemon,
+ List<Pokemon> usedUp, int bstBalanceLevel) {
+ // start with within 10% and add 5% either direction till we find
+ // something
+ int balancedBST = bstBalanceLevel * 10 + 250;
+ int currentBST = Math.min(current.bstForPowerLevels(), balancedBST);
+ int minTarget = currentBST - currentBST / 10;
+ int maxTarget = currentBST + currentBST / 10;
+ List<Pokemon> canPick = new ArrayList<>();
+ int expandRounds = 0;
+ while (canPick.isEmpty() || (canPick.size() < 3 && expandRounds < 3)) {
+ for (Pokemon pk : pokemonPool) {
+ if (pk.bstForPowerLevels() >= minTarget && pk.bstForPowerLevels() <= maxTarget
+ && (!banSamePokemon || pk != current) && (usedUp == null || !usedUp.contains(pk))
+ && !canPick.contains(pk)) {
+ canPick.add(pk);
+ }
+ }
+ minTarget -= currentBST / 20;
+ maxTarget += currentBST / 20;
+ expandRounds++;
+ }
+ return canPick.get(this.random.nextInt(canPick.size()));
+ }
+
+ private void setFormeForEncounter(Encounter enc, Pokemon pk) {
+ boolean checkCosmetics = true;
+ enc.formeNumber = 0;
+ if (enc.pokemon.formeNumber > 0) {
+ enc.formeNumber = enc.pokemon.formeNumber;
+ enc.pokemon = enc.pokemon.baseForme;
+ checkCosmetics = false;
+ }
+ if (checkCosmetics && enc.pokemon.cosmeticForms > 0) {
+ enc.formeNumber = enc.pokemon.getCosmeticFormNumber(this.random.nextInt(enc.pokemon.cosmeticForms));
+ } else if (!checkCosmetics && pk.cosmeticForms > 0) {
+ enc.formeNumber += pk.getCosmeticFormNumber(this.random.nextInt(pk.cosmeticForms));
+ }
+ }
+
+ private Map<Integer, List<EncounterSet>> mapZonesToEncounters(List<EncounterSet> encountersForAreas) {
+ Map<Integer, List<EncounterSet>> zonesToEncounters = new TreeMap<>();
+ for (EncounterSet encountersInArea : encountersForAreas) {
+ if (zonesToEncounters.containsKey(encountersInArea.offset)) {
+ zonesToEncounters.get(encountersInArea.offset).add(encountersInArea);
+ } else {
+ List<EncounterSet> encountersForZone = new ArrayList<>();
+ encountersForZone.add(encountersInArea);
+ zonesToEncounters.put(encountersInArea.offset, encountersForZone);
+ }
+ }
+ return zonesToEncounters;
+ }
+
+ public Pokemon pickEntirelyRandomPokemon(boolean includeFormes, boolean noLegendaries, EncounterSet area, List<Pokemon> banned) {
+ Pokemon result;
+ Pokemon randomNonLegendaryPokemon = includeFormes ? randomNonLegendaryPokemonInclFormes() : randomNonLegendaryPokemon();
+ Pokemon randomPokemon = includeFormes ? randomPokemonInclFormes() : randomPokemon();
+ result = noLegendaries ? randomNonLegendaryPokemon : randomPokemon;
+ while (result.actuallyCosmetic) {
+ randomNonLegendaryPokemon = includeFormes ? randomNonLegendaryPokemonInclFormes() : randomNonLegendaryPokemon();
+ randomPokemon = includeFormes ? randomPokemonInclFormes() : randomPokemon();
+ result = noLegendaries ? randomNonLegendaryPokemon : randomPokemon;
+ }
+ while (banned.contains(result) || area.bannedPokemon.contains(result)) {
+ randomNonLegendaryPokemon = includeFormes ? randomNonLegendaryPokemonInclFormes() : randomNonLegendaryPokemon();
+ randomPokemon = includeFormes ? randomPokemonInclFormes() : randomPokemon();
+ result = noLegendaries ? randomNonLegendaryPokemon : randomPokemon;
+ while (result.actuallyCosmetic) {
+ randomNonLegendaryPokemon = includeFormes ? randomNonLegendaryPokemonInclFormes() : randomNonLegendaryPokemon();
+ randomPokemon = includeFormes ? randomPokemonInclFormes() : randomPokemon();
+ result = noLegendaries ? randomNonLegendaryPokemon : randomPokemon;
+ }
+ }
+ return result;
+ }
+
+ private Pokemon pickStaticPowerLvlReplacement(List<Pokemon> pokemonPool, Pokemon current, boolean banSamePokemon,
+ boolean limitBST) {
+ // start with within 10% and add 5% either direction till we find
+ // something
+ int currentBST = current.bstForPowerLevels();
+ int minTarget = limitBST ? currentBST - currentBST / 5 : currentBST - currentBST / 10;
+ int maxTarget = limitBST ? currentBST : currentBST + currentBST / 10;
+ List<Pokemon> canPick = new ArrayList<>();
+ int expandRounds = 0;
+ while (canPick.isEmpty() || (canPick.size() < 3 && expandRounds < 3)) {
+ for (Pokemon pk : pokemonPool) {
+ if (pk.bstForPowerLevels() >= minTarget && pk.bstForPowerLevels() <= maxTarget
+ && (!banSamePokemon || pk != current) && !canPick.contains(pk)) {
+ canPick.add(pk);
+ }
+ }
+ minTarget -= currentBST / 20;
+ maxTarget += currentBST / 20;
+ expandRounds++;
+ }
+ return canPick.get(this.random.nextInt(canPick.size()));
+ }
+
+ @Override
+ public List<Pokemon> getAbilityDependentFormes() {
+ List<Pokemon> abilityDependentFormes = new ArrayList<>();
+ for (int i = 0; i < mainPokemonListInclFormes.size(); i++) {
+ Pokemon pokemon = mainPokemonListInclFormes.get(i);
+ if (pokemon.baseForme != null) {
+ if (pokemon.baseForme.number == Species.castform) {
+ // All alternate Castform formes
+ abilityDependentFormes.add(pokemon);
+ } else if (pokemon.baseForme.number == Species.darmanitan && pokemon.formeNumber == 1) {
+ // Damanitan-Z
+ abilityDependentFormes.add(pokemon);
+ } else if (pokemon.baseForme.number == Species.aegislash) {
+ // Aegislash-B
+ abilityDependentFormes.add(pokemon);
+ } else if (pokemon.baseForme.number == Species.wishiwashi) {
+ // Wishiwashi-S
+ abilityDependentFormes.add(pokemon);
+ }
+ }
+ }
+ return abilityDependentFormes;
+ }
+
+ @Override
+ public List<Pokemon> getBannedFormesForPlayerPokemon() {
+ List<Pokemon> bannedFormes = new ArrayList<>();
+ for (int i = 0; i < mainPokemonListInclFormes.size(); i++) {
+ Pokemon pokemon = mainPokemonListInclFormes.get(i);
+ if (pokemon.baseForme != null) {
+ if (pokemon.baseForme.number == Species.giratina) {
+ // Giratina-O is banned because it reverts back to Altered Forme if
+ // equipped with any item that isn't the Griseous Orb.
+ bannedFormes.add(pokemon);
+ } else if (pokemon.baseForme.number == Species.shaymin) {
+ // Shaymin-S is banned because it reverts back to its original forme
+ // under a variety of circumstances, and can only be changed back
+ // with the Gracidea.
+ bannedFormes.add(pokemon);
+ }
+ }
+ }
+ return bannedFormes;
+ }
+
+ @Override
+ public void randomizeTotemPokemon(Settings settings) {
+ boolean randomizeTotem =
+ settings.getTotemPokemonMod() == Settings.TotemPokemonMod.RANDOM ||
+ settings.getTotemPokemonMod() == Settings.TotemPokemonMod.SIMILAR_STRENGTH;
+ boolean randomizeAllies =
+ settings.getAllyPokemonMod() == Settings.AllyPokemonMod.RANDOM ||
+ settings.getAllyPokemonMod() == Settings.AllyPokemonMod.SIMILAR_STRENGTH;
+ boolean randomizeAuras =
+ settings.getAuraMod() == Settings.AuraMod.RANDOM ||
+ settings.getAuraMod() == Settings.AuraMod.SAME_STRENGTH;
+ boolean similarStrengthTotem = settings.getTotemPokemonMod() == Settings.TotemPokemonMod.SIMILAR_STRENGTH;
+ boolean similarStrengthAllies = settings.getAllyPokemonMod() == Settings.AllyPokemonMod.SIMILAR_STRENGTH;
+ boolean similarStrengthAuras = settings.getAuraMod() == Settings.AuraMod.SAME_STRENGTH;
+ boolean randomizeHeldItems = settings.isRandomizeTotemHeldItems();
+ int levelModifier = settings.isTotemLevelsModified() ? settings.getTotemLevelModifier() : 0;
+ boolean allowAltFormes = settings.isAllowTotemAltFormes();
+ boolean banIrregularAltFormes = settings.isBanIrregularAltFormes();
+ boolean abilitiesAreRandomized = settings.getAbilitiesMod() == Settings.AbilitiesMod.RANDOMIZE;
+
+ checkPokemonRestrictions();
+ List<TotemPokemon> currentTotemPokemon = this.getTotemPokemon();
+ List<TotemPokemon> replacements = new ArrayList<>();
+ List<Pokemon> banned = this.bannedForStaticPokemon();
+ if (!abilitiesAreRandomized) {
+ List<Pokemon> abilityDependentFormes = getAbilityDependentFormes();
+ banned.addAll(abilityDependentFormes);
+ }
+ if (banIrregularAltFormes) {
+ banned.addAll(getIrregularFormes());
+ }
+ List<Pokemon> listInclFormesExclCosmetics =
+ mainPokemonListInclFormes
+ .stream()
+ .filter(pk -> !pk.actuallyCosmetic)
+ .collect(Collectors.toList());
+ List<Pokemon> pokemonLeft = new ArrayList<>(!allowAltFormes ? mainPokemonList : listInclFormesExclCosmetics);
+ pokemonLeft.removeAll(banned);
+ for (TotemPokemon old : currentTotemPokemon) {
+ TotemPokemon newTotem = new TotemPokemon();
+ newTotem.heldItem = old.heldItem;
+ if (randomizeTotem) {
+ Pokemon newPK;
+ Pokemon oldPK = old.pkmn;
+ if (old.forme > 0) {
+ oldPK = getAltFormeOfPokemon(oldPK, old.forme);
+ }
+
+ if (similarStrengthTotem) {
+ newPK = pickStaticPowerLvlReplacement(
+ pokemonLeft,
+ oldPK,
+ true,
+ false);
+ } else {
+ newPK = pokemonLeft.remove(this.random.nextInt(pokemonLeft.size()));
+ }
+
+ pokemonLeft.remove(newPK);
+ newTotem.pkmn = newPK;
+ setFormeForStaticEncounter(newTotem, newPK);
+ newTotem.resetMoves = true;
+ newTotem.level = old.level;
+
+ if (levelModifier != 0) {
+ newTotem.level = Math.min(100, (int) Math.round(newTotem.level * (1 + levelModifier / 100.0)));
+ }
+ if (pokemonLeft.size() == 0) {
+ pokemonLeft.addAll(!allowAltFormes ? mainPokemonList : listInclFormesExclCosmetics);
+ pokemonLeft.removeAll(banned);
+ }
+ } else {
+ newTotem.pkmn = old.pkmn;
+ newTotem.level = old.level;
+ if (levelModifier != 0) {
+ newTotem.level = Math.min(100, (int) Math.round(newTotem.level * (1 + levelModifier / 100.0)));
+ }
+ setFormeForStaticEncounter(newTotem, newTotem.pkmn);
+ }
+
+ if (randomizeAllies) {
+ for (Integer oldAllyIndex: old.allies.keySet()) {
+ StaticEncounter oldAlly = old.allies.get(oldAllyIndex);
+ StaticEncounter newAlly = new StaticEncounter();
+ Pokemon newAllyPK;
+ Pokemon oldAllyPK = oldAlly.pkmn;
+ if (oldAlly.forme > 0) {
+ oldAllyPK = getAltFormeOfPokemon(oldAllyPK, oldAlly.forme);
+ }
+ if (similarStrengthAllies) {
+ newAllyPK = pickStaticPowerLvlReplacement(
+ pokemonLeft,
+ oldAllyPK,
+ true,
+ false);
+ } else {
+ newAllyPK = pokemonLeft.remove(this.random.nextInt(pokemonLeft.size()));
+ }
+
+ pokemonLeft.remove(newAllyPK);
+ newAlly.pkmn = newAllyPK;
+ setFormeForStaticEncounter(newAlly, newAllyPK);
+ newAlly.resetMoves = true;
+ newAlly.level = oldAlly.level;
+ if (levelModifier != 0) {
+ newAlly.level = Math.min(100, (int) Math.round(newAlly.level * (1 + levelModifier / 100.0)));
+ }
+
+ newTotem.allies.put(oldAllyIndex,newAlly);
+ if (pokemonLeft.size() == 0) {
+ pokemonLeft.addAll(!allowAltFormes ? mainPokemonList : listInclFormesExclCosmetics);
+ pokemonLeft.removeAll(banned);
+ }
+ }
+ } else {
+ newTotem.allies = old.allies;
+ for (StaticEncounter ally: newTotem.allies.values()) {
+ if (levelModifier != 0) {
+ ally.level = Math.min(100, (int) Math.round(ally.level * (1 + levelModifier / 100.0)));
+ setFormeForStaticEncounter(ally, ally.pkmn);
+ }
+ }
+ }
+
+ if (randomizeAuras) {
+ if (similarStrengthAuras) {
+ newTotem.aura = Aura.randomAuraSimilarStrength(this.random, old.aura);
+ } else {
+ newTotem.aura = Aura.randomAura(this.random);
+ }
+ } else {
+ newTotem.aura = old.aura;
+ }
+
+ if (randomizeHeldItems) {
+ if (old.heldItem != 0) {
+ List<Integer> consumableList = getAllConsumableHeldItems();
+ newTotem.heldItem = consumableList.get(this.random.nextInt(consumableList.size()));
+ }
+ }
+
+ replacements.add(newTotem);
+ }
+
+ // Save
+ this.setTotemPokemon(replacements);
+ }
+
+ /* Helper methods used by subclasses and/or this class */
+
+ void checkPokemonRestrictions() {
+ if (!restrictionsSet) {
+ setPokemonPool(null);
+ }
+ }
+
+ protected void applyCamelCaseNames() {
+ List<Pokemon> pokes = getPokemon();
+ for (Pokemon pkmn : pokes) {
+ if (pkmn == null) {
+ continue;
+ }
+ pkmn.name = RomFunctions.camelCase(pkmn.name);
+ }
+
+ }
+
+ private void setPlacementHistory(Pokemon newPK) {
+ Integer history = getPlacementHistory(newPK);
+ placementHistory.put(newPK, history + 1);
+ }
+
+ private int getPlacementHistory(Pokemon newPK) {
+ return placementHistory.getOrDefault(newPK, 0);
+ }
+
+ private double getPlacementAverage() {
+ return placementHistory.values().stream().mapToInt(e -> e).average().orElse(0);
+ }
+
+
+ private List<Pokemon> getBelowAveragePlacements() {
+ // This method will return a PK if the number of times a pokemon has been
+ // placed is less than average of all placed pokemon's appearances
+ // E.g., Charmander's been placed once, but the average for all pokemon is 2.2
+ // So add to list and return
+
+ List<Pokemon> toPlacePK = new ArrayList<>();
+ List<Pokemon> placedPK = new ArrayList<>(placementHistory.keySet());
+ List<Pokemon> allPK = cachedAllList;
+ int placedPKNum = 0;
+ for (Pokemon p : placedPK) {
+ placedPKNum += placementHistory.get(p);
+ }
+ float placedAverage = Math.round((float)placedPKNum / (float)placedPK.size());
+
+
+
+ if (placedAverage != placedAverage) { // this is checking for NaN, should only happen on first call
+ placedAverage = 1;
+ }
+
+ // now we've got placement average, iterate all pokemon and see if they qualify to be placed
+
+ for (Pokemon newPK : allPK) {
+ if (placedPK.contains(newPK)) { // if it's in the list of previously placed, then check its viability
+ if (placementHistory.get(newPK) <= placedAverage) {
+ toPlacePK.add(newPK);
+ }
+ }
+ else {
+ toPlacePK.add(newPK); // if not placed at all, automatically flag true for placing
+
+ }
+ }
+
+ return toPlacePK;
+
+ }
+
+ @Override
+ public void renderPlacementHistory() {
+ List<Pokemon> placedPK = new ArrayList<>(placementHistory.keySet());
+ for (Pokemon p : placedPK) {
+ System.out.println(p.name+": "+ placementHistory.get(p));
+ }
+ }
+
+ ///// Item functions
+ private void setItemPlacementHistory(int newItem) {
+ Integer history = getItemPlacementHistory(newItem);
+ // System.out.println("Current history: " + newPK.name + " : " + history);
+ itemPlacementHistory.put(newItem, history + 1);
+ }
+
+ private int getItemPlacementHistory(int newItem) {
+ List<Integer> placedItem = new ArrayList<>(itemPlacementHistory.keySet());
+ if (placedItem.contains(newItem)) {
+ return itemPlacementHistory.get(newItem);
+ }
+ else {
+ return 0;
+ }
+ }
+
+ private float getItemPlacementAverage() {
+ // This method will return an integer of average for itemPlacementHistory
+ // placed is less than average of all placed pokemon's appearances
+ // E.g., Charmander's been placed once, but the average for all pokemon is 2.2
+ // So add to list and return
+
+ List<Integer> placedPK = new ArrayList<>(itemPlacementHistory.keySet());
+ int placedPKNum = 0;
+ for (Integer p : placedPK) {
+ placedPKNum += itemPlacementHistory.get(p);
+ }
+ return (float)placedPKNum / (float)placedPK.size();
+ }
+
+ private void reportItemHistory() {
+ String[] itemNames = this.getItemNames();
+ List<Integer> placedItem = new ArrayList<>(itemPlacementHistory.keySet());
+ for (Integer p : placedItem) {
+ System.out.println(itemNames[p]+": "+ itemPlacementHistory.get(p));
+ }
+ }
+
+ protected void log(String log) {
+ if (logStream != null) {
+ logStream.println(log);
+ }
+ }
+
+ protected void logBlankLine() {
+ if (logStream != null) {
+ logStream.println();
+ }
+ }
+
+ /* Default Implementations */
+ /* Used when a subclass doesn't override */
+ /*
+ * The implication here is that these WILL be overridden by at least one
+ * subclass.
+ */
+ @Override
+ public boolean typeInGame(Type type) {
+ return !type.isHackOnly && !(type == Type.FAIRY && generationOfPokemon() < 6);
+ }
+
+ @Override
+ public String abilityName(int number) {
+ return "";
+ }
+
+ @Override
+ public List<Integer> getUselessAbilities() {
+ return new ArrayList<>();
+ }
+
+ @Override
+ public int getAbilityForTrainerPokemon(TrainerPokemon tp) {
+ return 0;
+ }
+
+ @Override
+ public boolean hasTimeBasedEncounters() {
+ // DEFAULT: no
+ return false;
+ }
+
+ @Override
+ public List<Pokemon> bannedForWildEncounters() {
+ return new ArrayList<>();
+ }
+
+ @Override
+ public List<Integer> getMovesBannedFromLevelup() {
+ return new ArrayList<>();
+ }
+
+ @Override
+ public List<Pokemon> bannedForStaticPokemon() {
+ return new ArrayList<>();
+ }
+
+ @Override
+ public boolean forceSwapStaticMegaEvos() {
+ return false;
+ }
+
+ @Override
+ public int maxTrainerNameLength() {
+ // default: no real limit
+ return Integer.MAX_VALUE;
+ }
+
+ @Override
+ public int maxSumOfTrainerNameLengths() {
+ // default: no real limit
+ return Integer.MAX_VALUE;
+ }
+
+ @Override
+ public int maxTrainerClassNameLength() {
+ // default: no real limit
+ return Integer.MAX_VALUE;
+ }
+
+ @Override
+ public int maxTradeNicknameLength() {
+ return 10;
+ }
+
+ @Override
+ public int maxTradeOTNameLength() {
+ return 7;
+ }
+
+ @Override
+ public boolean altFormesCanHaveDifferentEvolutions() {
+ return false;
+ }
+
+ @Override
+ public List<Integer> getGameBreakingMoves() {
+ // Sonicboom & Dragon Rage
+ return Arrays.asList(49, 82);
+ }
+
+ @Override
+ public List<Integer> getIllegalMoves() {
+ return new ArrayList<>();
+ }
+
+ @Override
+ public boolean isYellow() {
+ return false;
+ }
+
+ @Override
+ public void writeCheckValueToROM(int value) {
+ // do nothing
+ }
+
+ @Override
+ public int miscTweaksAvailable() {
+ // default: none
+ return 0;
+ }
+
+ @Override
+ public void applyMiscTweaks(Settings settings) {
+ int selectedMiscTweaks = settings.getCurrentMiscTweaks();
+
+ int codeTweaksAvailable = miscTweaksAvailable();
+ List<MiscTweak> tweaksToApply = new ArrayList<>();
+
+ for (MiscTweak mt : MiscTweak.allTweaks) {
+ if ((codeTweaksAvailable & mt.getValue()) > 0 && (selectedMiscTweaks & mt.getValue()) > 0) {
+ tweaksToApply.add(mt);
+ }
+ }
+
+ // Sort so priority is respected in tweak ordering.
+ Collections.sort(tweaksToApply);
+
+ // Now apply in order.
+ for (MiscTweak mt : tweaksToApply) {
+ applyMiscTweak(mt);
+ }
+ }
+
+ @Override
+ public void applyMiscTweak(MiscTweak tweak) {
+ // default: do nothing
+ }
+
+ @Override
+ public List<Integer> getXItems() {
+ return GlobalConstants.xItems;
+ }
+
+ @Override
+ public List<Integer> getSensibleHeldItemsFor(TrainerPokemon tp, boolean consumableOnly, List<Move> moves, Map<Integer, List<MoveLearnt>> movesets) {
+ return Arrays.asList(0);
+ }
+
+ @Override
+ public List<Integer> getAllConsumableHeldItems() {
+ return Arrays.asList(0);
+ }
+
+ @Override
+ public List<Integer> getAllHeldItems() {
+ return Arrays.asList(0);
+ }
+
+ @Override
+ public List<Pokemon> getBannedFormesForTrainerPokemon() {
+ return new ArrayList<>();
+ }
+
+ @Override
+ public List<PickupItem> getPickupItems() {
+ return new ArrayList<>();
+ }
+
+ @Override
+ public void setPickupItems(List<PickupItem> pickupItems) {
+ // do nothing
+ }
+}