Last active
June 29, 2026 02:51
-
-
Save sora10pls/07d69600241f3a47959b802560ab8900 to your computer and use it in GitHub Desktop.
A Randomizer for Pokémon Scarlet and Pokémon Violet
This file has been truncated, but you can view the full file.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| // PATHS | |
| private static string DEST_MASTER = @""; | |
| private static string PATH_MASTER = @""; | |
| private static string PATH_TRPFD = @""; | |
| private static string PATH_ADDITIONAL_MODS = @""; | |
| private static string LANGUAGE = "English"; // VALID OPTIONS: English, French, German, Italian, JPN, JPN_KANJI, Korean, Simp_Chinese, Spanish, Trad_Chinese | |
| // RANDOMIZER GENERATION SETTINGS | |
| private static bool AUTOMATICALLY_PATCH_TRPFD = true; | |
| private static bool GENERATE_SPOILER_LOG_FILE = true; | |
| private static bool RANDOMIZE_RANDOMIZER_SETTINGS = false; | |
| private static bool UPDATE_IN_GAME_TEXT_FOR_ALL_LANGUAGES = false; | |
| private static byte NUMBER_OF_RANDOMIZERS_TO_GENERATE = 1; | |
| private const string RANDOMIZER_SEED = ""; // Leave empty to seed according to current time. | |
| // POKÉMON AVAILABILITY SETTINGS | |
| private static bool ALLOW_SPECIES_GEN_1 = true; | |
| private static bool ALLOW_SPECIES_GEN_2 = true; | |
| private static bool ALLOW_SPECIES_GEN_3 = true; | |
| private static bool ALLOW_SPECIES_GEN_4 = true; | |
| private static bool ALLOW_SPECIES_GEN_5 = true; | |
| private static bool ALLOW_SPECIES_GEN_6 = true; | |
| private static bool ALLOW_SPECIES_GEN_7 = true; | |
| private static bool ALLOW_SPECIES_GEN_8 = true; | |
| private static bool ALLOW_SPECIES_GEN_9 = true; | |
| private static bool ALLOW_SPECIES_LEGENDARY = true; | |
| private static bool ALLOW_SPECIES_MYTHICAL = true; | |
| private static bool ALLOW_SPECIES_PARADOX = true; | |
| // SWAP CERTAIN POKÉMON FOR OTHER POKÉMON OF THAT SAME GROUP | |
| private static bool FORCE_SWAP_SPECIES_LEGENDARY = true; | |
| private static bool FORCE_SWAP_SPECIES_MYTHICAL = true; | |
| private static bool FORCE_SWAP_SPECIES_PARADOX = true; | |
| private static bool FORCE_SWAP_SPECIES_ALL_SPECIAL_POKEMON = false; | |
| // POKÉMON STATS SETTINGS | |
| private static bool RANDOMIZE_POKEMON_ABILITIES = false; | |
| private static bool RANDOMIZE_POKEMON_TYPES = false; | |
| private static BaseStatRandomizer RANDOMIZE_POKEMON_BASE_STATS = BaseStatRandomizer.DoNotRandomize; // VALID OPTIONS: DoNotRandomize, Shuffled, WithinBST, CompletelyRandom | |
| private static EvolutionRandomizer RANDOMIZE_POKEMON_EVOLUTIONS = EvolutionRandomizer.DoNotRandomize; // VALID OPTIONS: DoNotRandomize, Randomize, EvolveEveryLevel | |
| private static LearnsetRandomizer RANDOMIZE_POKEMON_LEARNSETS = LearnsetRandomizer.DoNotRandomize; // VALID OPTIONS: DoNotRandomize, Randomize, MetronomeOnly | |
| // POKÉMON ABILITY SETTINGS | |
| private static bool ALLOW_RANDOM_HIDDEN_ABILITIES = false; | |
| private static bool BAN_ABILITY_WONDER_GUARD = false; | |
| private static bool BAN_DOUBLE_BATTLE_ABILITIES = false; | |
| private static bool BAN_TRAPPING_ABILITIES = false; | |
| private static bool COMBINE_DUPLICATE_ABILITIES = false; | |
| private static bool DISPLAY_ABILITIES_IN_POKEDEX = false; | |
| private static bool PRESERVE_FORM_CHANGE_ABILITIES = false; | |
| private static bool PRESERVE_OGERPON_EMBODY_ASPECT_ABILITIES = false; | |
| private static bool RANDOM_ABILITIES_FOLLOW_EVOLUTIONS = false; | |
| // POKÉMON TYPE SETTINGS | |
| private static byte GAIN_RANDOM_TYPE_EVO_1_CHANCE_PERCENT = 35; | |
| private static byte GAIN_RANDOM_TYPE_EVO_2_CHANCE_PERCENT = 55; | |
| private static byte RANDOM_DUAL_TYPE_CHANCE_PERCENT = 40; // This setting also applies to Starmobiles, if dual-type Starmobiles are enabled! | |
| private static bool RANDOM_TYPES_FOLLOW_EVOLUTIONS = false; | |
| // POKÉMON EVOLUTION SETTINGS | |
| private static bool MAKE_EVOLUTIONS_EASIER = false; | |
| private static bool REMOVE_TIME_BASED_EVOLUTIONS = false; | |
| private static EXPGroup RANDOM_EVOLUTION_EXP_GROWTH_RATE = EXPGroup.Slow; // VALID OPTIONS: MediumFast, Erratic, Fluctuating, MediumSlow, Fast, Slow | |
| // POKÉMON LEARNSET SETTINGS | |
| private static bool ALLOW_STARMOBILE_TORQUE_MOVES = false; // This setting also applies to randomized TMs, regardless of if learnsets are randomized or not! | |
| private static bool DO_NOT_RANDOMIZE_DITTO_LEARNSET = false; | |
| private static bool DO_NOT_RANDOMIZE_SMEARGLE_LEARNSET = false; | |
| private static bool GUARANTEE_FOUR_MOVES_AT_LEVEL_ONE = false; | |
| private static bool RANDOMIZE_LEARNSETS_WITH_TYPE_BIAS = false; | |
| private static byte RANDOM_LEARNSET_TYPE_BIAS_PERCENT = 60; | |
| private static bool STANDARDIZE_LEVELS_FOR_LEARNING_MOVES = false; | |
| // TECHNICAL MACHINE (TM) SETTINGS | |
| private static bool DO_NOT_RANDOMIZE_TERA_BLAST_TM = false; | |
| private static bool MAKE_ALL_TMs_CRAFTABLE_FROM_START = false; | |
| private static bool RANDOMIZE_TM_RECIPES = false; | |
| private static TMCompatibilityRandomizer TM_COMPATIBILITY = TMCompatibilityRandomizer.Default; // VALID OPTIONS: Default, Randomized, RandomizedPreferSameType, FullCompatibility | |
| private static bool RANDOMIZED_TM_COMPATIBILITY_FOLLOW_EVOLUTIONS = false; | |
| // TRAINER SETTINGS | |
| private static bool GIVE_IMPORTANT_TRAINERS_SIX_POKEMON = false; | |
| private static bool GIVE_TRAINERS_MAXIMUM_AI = false; | |
| private static bool GIVE_TRAINERS_MAX_INDIVIDUAL_VALUES = false; | |
| private static bool GIVE_TRAINERS_SMART_EFFORT_VALUES = false; | |
| private static bool MAKE_ALL_TRAINER_BATTLES_DOUBLE_BATTLES = false; | |
| private static bool REMOVE_FORCED_BONUS_STAT_CHANGES = false; | |
| private static bool REMOVE_IN_BATTLE_TRAINER_DIALOGUE = false; | |
| private static bool RIVAL_CARRIES_STARTER_THROUGHOUT_GAME = false; | |
| private static TypeThemeRandomizer TYPE_THEMED_IMPORTANT_TRAINERS = TypeThemeRandomizer.NotTypeThemed; // VALID OPTIONS: NotTypeThemed, VanillaTypeThemes, RandomTypeThemes | |
| private static bool FORCE_TERASTALLIZED_POKEMON_TO_HAVE_ONE_STAB_TERA_MOVE = false; | |
| private static TerastalMoveRandomizer TERASTAL_EXTRA_MOVE_SOURCE = TerastalMoveRandomizer.DefaultLearnset; // VALID OPTIONS: DefaultLearnset, AddVanillaTMs, AddRandomizedTMs | |
| private static byte ADD_NUMBER_OF_POKEMON_TO_REGULAR_TRAINERS = 0; | |
| private static byte ADD_NUMBER_OF_POKEMON_TO_IMPORTANT_TRAINERS = 0; | |
| private static byte TRAINER_MINIMUM_POKEMON_COUNT = 1; | |
| private static byte TRAINER_MAXIMUM_POKEMON_COUNT = 6; | |
| private static byte TRAINER_POKEMON_SHINY_CHANCE_PERCENT = 2; | |
| private static TrainerHeldItemRandomizer TRAINER_HELD_ITEM_DISTRIBUTION = TrainerHeldItemRandomizer.NoHeldItems; // VALID OPTIONS: NoHeldItems, UnchangedHeldItems, ImportantTrainersOnly, AllTrainers | |
| // TRAINER NAME SETTINGS — ONLY ENGLISH IS SUPPORTED AT THIS TIME | |
| private static bool RANDOMIZE_TRAINER_NAMES = false; | |
| private static bool ONLY_USE_CUSTOM_TRAINER_NAMES = false; | |
| private static string PATH_TEXT_FILE_CUSTOM_TRAINER_NAMES = @""; | |
| // TRAINER CLASS SETTINGS — ONLY ENGLISH IS SUPPORTED AT THIS TIME | |
| private static bool RANDOMIZE_TRAINER_CLASSES = false; | |
| private static bool ONLY_USE_CUSTOM_TRAINER_CLASSES = false; | |
| private static string PATH_TEXT_FILE_CUSTOM_TRAINER_CLASSES = @""; | |
| // MOVE PROPERTIES SETTINGS | |
| private static MovePropertyUpdater UPDATE_MOVE_PROPERTIES_TO_FUTURE_GAME = MovePropertyUpdater.DoNotUpdate; // VALID OPTIONS: DoNotUpdate, Champions | |
| private static bool UPDATE_MOVE_PP = false; | |
| private static bool UPDATE_MOVE_SECONDARY_EFFECTS = false; | |
| private static bool RANDOMIZE_MOVE_PROPERTIES_ACCURACY = false; | |
| private static bool RANDOMIZE_MOVE_PROPERTIES_CATEGORY = false; | |
| private static bool RANDOMIZE_MOVE_PROPERTIES_POWER = false; | |
| private static bool RANDOMIZE_MOVE_PROPERTIES_PP = false; | |
| private static bool RANDOMIZE_MOVE_PROPERTIES_TYPE = false; | |
| // THE POKÉMON YOU MEET BY INLET GROTTO | |
| private static bool RANDOMIZE_KORAIDON_MIRAIDON = false; | |
| // INTERACTABLE POKÉMON IN CITIES AND TOWNS | |
| private static bool RANDOMIZE_GENERIC_OVERWORLD_POKEMON = false; | |
| private static byte GENERIC_OVERWORLD_POKEMON_SHINY_CHANCE_PERCENT = 2; | |
| // EXTRA MODIFICATIONS | |
| private static bool FORCE_FULLY_EVOLVED_POKEMON = false; | |
| private static byte FORCE_FULLY_EVOLVED_POKEMON_LEVEL = 45; | |
| private static bool MODIFY_POKEMON_LEVELS = false; | |
| private static sbyte LEVEL_MODIFIER_PERCENT = 10; // Values can be positive for a level increase, or negative for a level decrease! | |
| private static bool ALLOW_FULLY_RANDOM_TERA_TYPES = false; | |
| private static byte STELLAR_TERA_TYPE_CHANCE_PERCENT = 2; | |
| // ADDITIONAL RANDOMIZERS | |
| private static bool RANDOMIZE_POKEMON_TO_HAVE_SIMILAR_STRENGTH = false; | |
| private static bool RANDOMIZE_RESTAURANT_FOOD_MEAL_POWERS = false; | |
| private static bool RANDOMIZE_ROAMING_FORM_GIMMIGHOUL = false; | |
| private static bool RANDOMIZE_SANDWICH_SHOP_INVENTORIES = false; | |
| private static bool RANDOMIZE_STARTERS_INTO_OTHER_STARTERS = false; | |
| private static bool RANDOMIZE_TERA_RAID_BONUS_ITEM_DROPS = false; | |
| private static bool RANDOMIZE_TERA_RAID_FIXED_ITEM_DROPS = false; | |
| // ADDITIONAL SETTINGS | |
| private static bool ADD_BLOODMOON_URSALUNA_EVOLUTION = false; // When enabled, Ursaring can evolve into Bloodmoon Ursaluna when exposed to a Foreign Treat. | |
| private static bool ALLOW_BATTLE_ONLY_FORMS = false; | |
| private static bool ALLOW_DUAL_TYPE_STARMOBILES = false; | |
| private static bool ALLOW_RANDOM_PICNIC_ITEMS = false; | |
| private static bool BALANCE_SHOP_ITEM_PRICES = false; | |
| private static bool ENSURE_STARTERS_HAVE_TWO_EVOLUTIONS = false; | |
| private static bool GIVE_OPPONENT_SMEARGLE_RANDOM_MOVESETS = false; | |
| private static bool GIVE_POKEMON_RANDOM_HELD_ITEMS = false; | |
| private static bool GIVE_POKEMON_RANDOM_POKE_BALLS = false; | |
| private static bool GIVE_POKEMON_RANDOM_RIBBONS_OR_MARKS = false; | |
| private static bool MAKE_FIRST_FIELD_ITEM_999_RARE_CANDIES = false; | |
| private static bool MAKE_WILD_POKEMON_TIME_OF_DAY_DEPENDENT = false; | |
| private static bool REDUCE_EGG_HATCH_CYCLES = false; | |
| private static bool REMOVE_DITTO_ZORUA_ZOROARK_WILD_DISGUISES = false; | |
| private static byte TARGET_STAR_BARRAGE_KO_COUNT_INITIAL = 30; | |
| private static byte TARGET_STAR_BARRAGE_KO_COUNT_REMATCH = 50; | |
| // ADDITIONAL DIFFICULTY SETTINGS | |
| private static bool FORCE_FULLY_EVOLVED_TITAN_POKEMON = false; | |
| private static bool GIVE_ALL_POKEMON_MAX_INDIVIDUAL_VALUES = false; | |
| private static bool GIVE_BOSS_POKEMON_SMART_HELD_ITEMS = false; | |
| private static bool REMOVE_EFFORT_VALUE_YIELDS = false; | |
| // =============================== | |
| // CUSTOM STARTER POKÉMON SETTINGS | |
| // =============================== | |
| // If you want completely random starters, you can leave these settings alone. | |
| // If you wish, it is still possible to keep the Pokémon species and form random, while still applying other forced criteria. | |
| // =============================== | |
| // Species: Input a National Pokédex number (e.g. for Meowth, set the value to 52), otherwise keep 0 for a randomly selected species. | |
| // Form: Input a form index (e.g. Kantonian Meowth is 0, Alolan Meowth is 1, Galarian Meowth is 2), otherwise keep -1 for a randomly selected form. | |
| // Gender: Input 1 for male, 2 for female, otherwise keep -1 if the Pokémon is gender unknown, or for the game to randomly assign a gender. | |
| // Ability: Input 1 for its first ability, 2 for its second ability, 3 for its Hidden Ability, otherwise keep -1 for the game to randomly assign an ability. | |
| // Nature: Input a nature name (e.g. Adamant, Modest, Jolly, Timid), otherwise keep "Random" for the game to randomly assign a nature. | |
| // Poké Ball: Input a Poké Ball name (e.g. Master, Poke, Safari, Moon, Beast), otherwise keep "Default". | |
| // Tera Type: Input a type name (e.g. Grass, Fire, Water, Stellar), otherwise keep "Default" for the game to assign a Tera Type. | |
| // Shiny: If the value is "true" then it will be guaranteed to be Shiny, and if the value is "false" then it can still be encountered as a Shiny, it is just not guaranteed. | |
| // =============================== | |
| // GRASS-TYPE STARTER POKÉMON CRITERIA | |
| private static StarterPokemonCriteria StarterCriteriaGrass = new() | |
| { | |
| Species = 0, | |
| Form = -1, | |
| Gender = -1, | |
| Ability = -1, | |
| Nature = Nature.Random, | |
| PokéBall = BallIndex.Default, | |
| TeraType = TypeIndex.Default, | |
| Shiny = false, | |
| }; | |
| // FIRE-TYPE STARTER POKÉMON CRITERIA | |
| private static StarterPokemonCriteria StarterCriteriaFire = new() | |
| { | |
| Species = 0, | |
| Form = -1, | |
| Gender = -1, | |
| Ability = -1, | |
| Nature = Nature.Random, | |
| PokéBall = BallIndex.Default, | |
| TeraType = TypeIndex.Default, | |
| Shiny = false, | |
| }; | |
| // WATER-TYPE STARTER POKÉMON CRITERIA | |
| private static StarterPokemonCriteria StarterCriteriaWater = new() | |
| { | |
| Species = 0, | |
| Form = -1, | |
| Gender = -1, | |
| Ability = -1, | |
| Nature = Nature.Random, | |
| PokéBall = BallIndex.Default, | |
| TeraType = TypeIndex.Default, | |
| Shiny = false, | |
| }; | |
| // ENABLING THESE SETTINGS CAN LEAD TO GAME CRASHES OR SOFTLOCKS — PROCEED WITH CAUTION! | |
| private static bool ALLOW_ALL_TRAINERS_TO_TERASTALLIZE = false; | |
| private static bool ALLOW_ALL_UNUSABLE_MOVES_TO_BE_USED = false; | |
| // MAXIMUMS — DO NOT MODIFY THESE LINES! | |
| private const ushort MAX_SPECIES_ID = 1025; | |
| private const ushort MAX_ABILITY_ID = 310; | |
| private const byte MAX_TM_COUNT = 229; | |
| private const byte MAX_TYPE_ID = 17; | |
| private const byte MAX_BALL_ID = 26; | |
| private const byte MAX_RIBBON_ID = 111; | |
| // INITIALIZING RANDOMIZER RESOURCES — DO NOT MODIFY THESE LINES! | |
| private static PersonalTable PersonalInfoVanilla = GetVanillaPersonalTable(); | |
| private static PersonalTable PersonalInfoEdited = GetVanillaPersonalTable(); | |
| private static WazaTable MoveInfoVanilla = GetVanillaMoveTable(); | |
| private static WazaTable MoveInfoEdited = GetVanillaMoveTable(); | |
| private static List<ushort> LegendaryPokemon = GetLegendaryList(); | |
| private static List<ushort> MythicalPokemon = GetMythicalList(); | |
| private static List<ushort> ParadoxPokemon = GetParadoxList(); | |
| private static List<ushort> SpecialPokemon = GetSpecialPokemonList(); | |
| private static List<DevID> SpeciesBanlist = GetSpeciesBanlist(); | |
| private static List<DevID> SpeciesList = GetSpeciesList(); | |
| private static List<int> PermittedMoves = GetPermittedMoves(); | |
| private static List<int> AbilityBanlist = GetAbilityBanlist(); | |
| private static List<int> TechnicalMachineItems = GetTMItemIndices(); | |
| private static List<int> TechnicalMachineMoves = []; | |
| private static List<ushort> ItemList = GetItemList(); | |
| private static List<string> SpoilerLog = []; | |
| private static List<byte> ThemedTypes = []; | |
| private static List<string> LanguagesToIterate = GetLanguagesToIterate(); | |
| private static List<string> MissingBehaviorDataSingle = []; | |
| private static List<string> MissingBehaviorDataGroup = []; | |
| private static List<string> MissingBehaviorDataSpecial = []; | |
| private static int RandomizerSeedValue = GetRandomizerSeedValue(); | |
| private static TextConfig TextConfig = new(GameVersion.SV); | |
| private static byte CurrentRandomizerIteration = 1; | |
| private static string TextSeparator = GetTextSeparator(LANGUAGE); | |
| private static string[] StringsAbility = GetStringsCommon(LANGUAGE, "tokusei"); | |
| private static string[] StringsForm = GetStringsCommon(LANGUAGE, "zkn_form"); | |
| private static string[] StringsItem = GetStringsCommon(LANGUAGE, "itemname"); | |
| private static string[] StringsLocation = GetStringsCommon(LANGUAGE, "place_name"); | |
| private static string[] StringsMapMain = GetStringsCommon(LANGUAGE, "ymap_main"); | |
| private static string[] StringsMapPoint = GetStringsCommon(LANGUAGE, "ymap_point_name"); | |
| private static string[] StringsMove = GetStringsCommon(LANGUAGE, "wazaname"); | |
| private static string[] StringsSpecies = GetStringsCommon(LANGUAGE, "monsname"); | |
| private static string[] StringsTrainerName = GetStringsCommon(LANGUAGE, "trname"); | |
| private static string[] StringsTrainerType = GetStringsCommon(LANGUAGE, "trtype"); | |
| private static string[] StringsType = GetStringsCommon(LANGUAGE, "typename"); | |
| private static string BatchRandomizerDestination = Path.Combine(DEST_MASTER, $"Randomizer {CurrentRandomizerIteration:000}"); | |
| private static List<(DevID Species, short Form)> CurrentlyAssignedStarters = []; | |
| private static ParamSet FlawlessIVSpread = new ParamSet { HP = 31, ATK = 31, DEF = 31, SPA = 31, SPD = 31, SPE = 31 }; | |
| private static ParamSet EmptyEVSpread = new ParamSet { HP = 0, ATK = 0, DEF = 0, SPA = 0, SPD = 0, SPE = 0 }; | |
| private static void Main() | |
| { | |
| if (!GetArePathsValid()) | |
| { | |
| "Path definitions are invalid. Please revise your setup at the top of the LINQPad script.\nAborting randomization.".Dump(); | |
| } | |
| else | |
| { | |
| if (NUMBER_OF_RANDOMIZERS_TO_GENERATE <= 1) | |
| { | |
| InitializeRandomization(); | |
| } | |
| else | |
| { | |
| for (int i = CurrentRandomizerIteration; i <= NUMBER_OF_RANDOMIZERS_TO_GENERATE; i++) | |
| { | |
| InitializeRandomization(); | |
| CurrentRandomizerIteration++; | |
| BatchRandomizerDestination = BatchRandomizerDestination.Replace($"Randomizer {i:000}", $"Randomizer {(i + 1):000}"); | |
| // refresh | |
| PersonalInfoEdited = GetVanillaPersonalTable(); | |
| MoveInfoEdited = GetVanillaMoveTable(); | |
| } | |
| } | |
| } | |
| } | |
| private static void InitializeRandomization() | |
| { | |
| SpoilerLog.Clear(); | |
| if (RANDOMIZE_RANDOMIZER_SETTINGS) | |
| RandomizeRandomizerSettings(); | |
| PrepareInitialChanges(); | |
| UpdateMoveProperties(); | |
| RandomizeMoveProperties(); | |
| RandomizePokemonStats(); | |
| "".Dump(); | |
| RandomizeWildPokemon(); | |
| RandomizeFixedSymbolEncounters(); | |
| RandomizeStaticEncounters(); | |
| RandomizeGiftEncounters(); | |
| RandomizeTrainerPokemon(); | |
| RandomizeTechnicalMachines(); | |
| RandomizeFieldItems(); | |
| RandomizeSparklingItems(); | |
| RandomizePickupItemTables(); | |
| RandomizeRummagingItems(); | |
| RandomizeTeraRaidEncounters(); | |
| RandomizeTradeEncounters(); | |
| RandomizeAcademyAceTournamentRewards(); | |
| RandomizePokedexMilestoneRewards(); | |
| RandomizeStarBarrageEncounters(); | |
| RandomizeStarmobileStats(); | |
| RandomizeOgreOustinRewards(); | |
| RandomizeSpecialCoachRewards(); | |
| RandomizePortoMarinadaAuctions(); | |
| RandomizeItemPrinterItems(); | |
| RandomizeTeraRaidItemDrops(); | |
| RandomizeLeagueRepRewards(); | |
| RandomizeRestaurantMealPowers(); | |
| "".Dump(); | |
| UpdateShopLineups(); | |
| UpdateItemData(); | |
| UpdateBlueberryQuests(); | |
| UpdateBoutiqueAndSalonLineups(); | |
| UpdateDropItemTable(); | |
| UpdateGlimmeringCharmTable(); | |
| UpdatePaldeaPokedex(); | |
| UpdateItemSortTables(); | |
| UpdateTrainerPopupText(); | |
| UpdateInGameText(); | |
| UpdateInBattleDialogue(); | |
| UpdateLuaLogic(); | |
| UpdatePokedexHabitatsMenu(); | |
| ApplyExtraChanges(); | |
| "".Dump(); | |
| if (GENERATE_SPOILER_LOG_FILE && SpoilerLog.Count != 0) | |
| GenerateSpoilerLog(); | |
| FinalizeRandomizerData(); | |
| } | |
| private static void RandomizeWildPokemon() | |
| { | |
| var pokeData = new[] | |
| { | |
| "world/data/encount/pokedata/pokedata/pokedata_array.bin", | |
| "world/data/encount/pokedata/pokedata_su1/pokedata_su1_array.bin", | |
| "world/data/encount/pokedata/pokedata_su2/pokedata_su2_array.bin", | |
| }; | |
| bool IsBisharp(ushort species) => species == 0625; | |
| bool IsEncounterDisguised(ushort species) => species is 0132 or 0570 or 0571; | |
| bool IsApplySameSpawnConditions(ushort species, sbyte form) => species is 0493 or 0664 or 0665 or 0666 or 0669 or 0670 or 0671 or 0774 or 0869 && form != 0; | |
| int GetMaxBiome(int area) => area == 2 ? 23 : 22; // Chargestone is exclusive to Blueberry Academy | |
| var vt = new VersionTable() { A = true, B = true }; | |
| bool ShouldRandomizeAreaZeroEncounterAgain(DevID dev, sbyte form, bool used, int index) | |
| { | |
| // Ogerpon: check for potential Tera Type mismatch | |
| if (dev == DevID.DEV_KAMENONI) | |
| { | |
| var types = GetPokemonTypes(dev, form); | |
| if (form == 0 && types.Type1 == (byte)TypeIndex.Grass && types.Type2 == (byte)TypeIndex.Grass) return false; | |
| if (form == 1 && types.Type1 == (byte)TypeIndex.Water && types.Type2 == (byte)TypeIndex.Water) return false; | |
| if (form == 2 && types.Type1 == (byte)TypeIndex.Fire && types.Type2 == (byte)TypeIndex.Fire) return false; | |
| if (form == 3 && types.Type1 == (byte)TypeIndex.Rock && types.Type2 == (byte)TypeIndex.Rock) return false; | |
| return true; | |
| } | |
| // Terapagos: Tera Type mismatch | |
| if (dev == DevID.DEV_KODAIGAME) | |
| return true; | |
| // add all Paradox Pokémon at the end | |
| if (index < 180 && ALLOW_SPECIES_GEN_9 && ALLOW_SPECIES_PARADOX && ParadoxPokemon.Contains(GetNationalDexID((ushort)dev))) | |
| return true; | |
| // keep all species unique, only if we have a large enough pool | |
| if (SpeciesList.Count >= 200 && used) | |
| return true; | |
| return false; | |
| } | |
| foreach (var t in pokeData) | |
| { | |
| var index = t.LastIndexOf("/") + 1; | |
| var f = t[index..]; | |
| var d = t[..index]; | |
| var area = pokeData.IndexOf(t); | |
| var maxBiome = GetMaxBiome(area); | |
| var location = GetRegionName(area); | |
| bool paldea = area == 0; | |
| bool blueberry = area == 2; | |
| $"Randomizing wild Pokémon ({location})...".Dump(); | |
| var file = Path.Combine(PATH_MASTER, d, f); | |
| var obj = FlatBufferConverter.DeserializeFrom<EncountPokeDataArray>(file); | |
| obj.Table.Clear(); // now we reconstruct PokeData from scratch! | |
| for (ushort dev = 0001; dev <= MAX_SPECIES_ID; dev++) | |
| { | |
| if (SpeciesBanlist.Contains((DevID)dev)) | |
| continue; | |
| var species = GetNationalDexID(dev); | |
| if (species == 1024) // prevent wild Terapagos due to the game generating an invalid Tera Type (Normal, not Stellar) | |
| continue; | |
| bool bisharp = IsBisharp(species); | |
| bool disguise = IsEncounterDisguised(species); | |
| var forms = GetFormCount(dev); | |
| for (byte form = 0; form < forms; form++) | |
| { | |
| if (!GetIsPresentInGame(dev, form)) | |
| continue; | |
| if (!IsFormValidForWildEncounter(dev, form)) | |
| continue; | |
| var bandType = GetBandType(bisharp, disguise); | |
| var bandMember = GetBandMember(bandType, (DevID)dev, (sbyte)form); | |
| var bandSpecies = bandMember.Species; | |
| var bandForm = bandMember.Form; | |
| var bandRate = GetBandRate(bandType, bisharp); | |
| // clean up | |
| if ((DevID)dev == bandSpecies && form == bandForm) | |
| bandType = BandType.SAME; | |
| var level = GetWildLevelRange(dev, form, blueberry); | |
| var enable = GetEnableTable(dev, form); | |
| var areas = GenerateAreaList(area, enable.UpWater); | |
| var biomes = GetRandomBiomes(maxBiome, areas, enable.UpWater); | |
| var entry = new EncountPokeData() | |
| { | |
| DevId = (DevID)dev, | |
| Sex = SexType.DEFAULT, | |
| Form = (sbyte)form, | |
| MinLevel = level.Min, | |
| MaxLevel = level.Max, | |
| LotValue = GetLotValue(dev, form, blueberry), | |
| Biome1 = biomes[0], | |
| LotValue1 = GetLotValueBiome(), | |
| Biome2 = biomes[1], | |
| LotValue2 = GetLotValueBiome(), | |
| Biome3 = biomes[2], | |
| LotValue3 = GetLotValueBiome(), | |
| Biome4 = biomes[3], | |
| LotValue4 = GetLotValueBiome(), | |
| Area = areas, | |
| LocationName = string.Empty, | |
| MinHeight = 0, | |
| MaxHeight = 0, | |
| Enable = enable, | |
| Time = GetTimeTable(), | |
| FlagName = string.Empty, | |
| BandRate = (short)bandRate, | |
| Band = bandType, | |
| BandPoke = bandSpecies, | |
| BandSex = SexType.DEFAULT, | |
| BandForm = bandForm, | |
| OutbreakLotValue = (sbyte)GetLotValue(dev, form, blueberry), | |
| PokeVoiceClassification = GetPokeVoiceClassification(), | |
| Version = vt, | |
| Item = GetBringItem((DevID)dev, (sbyte)form, area), | |
| }; | |
| obj.Table.Add(entry); | |
| } | |
| } | |
| // apply identical spawning conditions for all forms of Arceus, Scatterbug, Spewpa, Vivillon, Flabébé, Floette, Florges, Minior, and Alcremie | |
| foreach (var k in obj.Table.Where(z => IsApplySameSpawnConditions((ushort)z.DevId, z.Form))) | |
| { | |
| var pk = obj.Table.FirstOrDefault(z => z.DevId == k.DevId); // source from base form, then copy to all others | |
| k.LotValue = pk.LotValue; | |
| k.Biome1 = pk.Biome1; | |
| k.LotValue1 = pk.LotValue1; | |
| k.Biome2 = pk.Biome2; | |
| k.LotValue2 = pk.LotValue2; | |
| k.Biome3 = pk.Biome3; | |
| k.LotValue3 = pk.LotValue3; | |
| k.Biome4 = pk.Biome4; | |
| k.LotValue4 = pk.LotValue4; | |
| k.Area = pk.Area; | |
| } | |
| if (GENERATE_SPOILER_LOG_FILE) | |
| WriteWildChangesToLog(obj.Table, location.ToUpper()); | |
| // Area Zero only has 5 biomes, so let's curate a special list of 200 spawns separate from Paldea | |
| if (paldea) | |
| { | |
| location = GetRegionName(3); | |
| $"Randomizing wild Pokémon ({location})...".Dump(); | |
| const string AREA_ZERO_LOCATIONS = "a_w23_d01,a_w23_d02,a_w23_d03,a_w23_d04,a_w23_field_1,a_w23_field_2,a_w23_field_3"; | |
| List<DevID> Used = []; | |
| for (int i = 0; i < 200; i++) | |
| { | |
| var biomes = GetAreaZeroBiomes(); | |
| var dev = GetRandomSpecies(0); | |
| var form = (sbyte)GetRandomForm(dev); | |
| // re-randomize if the spawn requires it | |
| while (ShouldRandomizeAreaZeroEncounterAgain(dev, form, Used.Contains(dev), i)) | |
| { | |
| dev = GetRandomSpecies(0); | |
| form = (sbyte)GetRandomForm(dev); | |
| } | |
| // add all Paradox Pokémon | |
| if (i >= 180 && ALLOW_SPECIES_GEN_9 && ALLOW_SPECIES_PARADOX) | |
| { | |
| dev = (DevID)GetDevID((ushort)ParadoxPokemon[i - 180]); | |
| form = 0; | |
| } | |
| bool bisharp = IsBisharp((ushort)dev); | |
| bool disguise = IsEncounterDisguised((ushort)dev); | |
| var bandType = GetBandType(bisharp, disguise); | |
| var bandMember = GetBandMember(bandType, dev, form); | |
| var bandSpecies = bandMember.Species; | |
| var bandForm = bandMember.Form; | |
| var bandRate = GetBandRate(bandType, bisharp); | |
| // clean up | |
| if (dev == bandSpecies && form == bandForm) | |
| bandType = BandType.SAME; | |
| var entry = new EncountPokeData() | |
| { | |
| DevId = dev, | |
| Sex = SexType.DEFAULT, | |
| Form = form, | |
| MinLevel = 1, | |
| MaxLevel = 100, | |
| LotValue = GetWeightedLotValue(), | |
| Biome1 = biomes[0], | |
| LotValue1 = GetLotValueBiome(), | |
| Biome2 = biomes[1], | |
| LotValue2 = GetLotValueBiome(), | |
| Biome3 = biomes[2], | |
| LotValue3 = GetLotValueBiome(), | |
| Biome4 = biomes[3], | |
| LotValue4 = GetLotValueBiome(), | |
| Area = string.Empty, | |
| LocationName = AREA_ZERO_LOCATIONS, | |
| MinHeight = 0, | |
| MaxHeight = 0, | |
| Enable = GetEnableTable((ushort)dev, (byte)form), | |
| Time = GetTimeTable(), | |
| FlagName = string.Empty, | |
| BandRate = (short)bandRate, | |
| Band = bandType, | |
| BandPoke = bandSpecies, | |
| BandSex = SexType.DEFAULT, | |
| BandForm = bandForm, | |
| OutbreakLotValue = 0, // no outbreaks in Area Zero | |
| PokeVoiceClassification = GetPokeVoiceClassification(), | |
| Version = vt, | |
| Item = GetBringItem(dev, form, 0), | |
| }; | |
| obj.Table.Add(entry); | |
| Used.Add(entry.DevId); | |
| } | |
| if (GENERATE_SPOILER_LOG_FILE) | |
| { | |
| var encounters = obj.Table.Where(z => !string.IsNullOrWhiteSpace(z.LocationName)).ToList(); | |
| WriteWildChangesToLog(encounters, location.ToUpper()); | |
| } | |
| } | |
| var data = FlatBufferConverter.SerializeFrom(obj); | |
| var dest = Path.Combine(DEST_MASTER, d); | |
| if (!Directory.Exists(dest)) | |
| Directory.CreateDirectory(dest); | |
| File.WriteAllBytes(Path.Combine(dest, f), data); | |
| } | |
| if (MODIFY_POKEMON_LEVELS) | |
| ModifyWildPokemonLevels(); | |
| } | |
| private static (short Min, short Max) GetWildLevelRange(ushort species, short form, bool blueberry) | |
| { | |
| if (!RANDOMIZE_POKEMON_TO_HAVE_SIMILAR_STRENGTH || RANDOMIZE_POKEMON_BASE_STATS == BaseStatRandomizer.CompletelyRandom || blueberry) | |
| return (1, 100); | |
| var bst = GetBaseStatTotal(species, form); | |
| if (SpecialPokemon.Contains(GetNationalDexID(species))) | |
| { | |
| return bst switch | |
| { | |
| >= 200 and <= 400 => (15, 30), | |
| >= 450 and <= 480 => (20, 35), | |
| >= 500 and <= 555 => (25, 45), | |
| >= 570 and <= 580 => (30, 50), | |
| 590 => (35, 55), | |
| 600 => (40, 60), | |
| >= 660 and <= 680 => (50, 75), | |
| _ => (55, 100), | |
| }; | |
| } | |
| return bst switch | |
| { | |
| >= 180 and <= 300 => (1, 25), | |
| >= 302 and <= 395 => (10, 35), | |
| >= 400 and <= 450 => (20, 45), | |
| >= 452 and <= 468 => (30, 50), | |
| >= 470 and <= 499 => (35, 55), | |
| >= 500 and <= 540 => (40, 60), | |
| >= 550 and <= 590 => (45, 65), | |
| _ => (50, 100), | |
| }; | |
| } | |
| private static short GetLotValue(ushort species, byte form, bool blueberry) | |
| { | |
| if (!RANDOMIZE_POKEMON_TO_HAVE_SIMILAR_STRENGTH || RANDOMIZE_POKEMON_BASE_STATS == BaseStatRandomizer.CompletelyRandom || blueberry) | |
| return GetWeightedLotValue(); | |
| var bst = GetBaseStatTotal(species, form); | |
| return bst switch | |
| { | |
| >= 001 and <= 300 => (short)GetRandom(40, 81), | |
| >= 301 and <= 450 => (short)GetRandom(20, 46), | |
| >= 451 and <= 520 => (short)GetRandom(15, 21), | |
| >= 521 and <= 549 => (short)GetRandom(07, 12), | |
| _ => (short)GetRandom(01, 05), | |
| }; | |
| } | |
| private static short GetWeightedLotValue() => GetRandom(100) switch | |
| { | |
| <= 99 and >= 75 => (short)GetRandom(60, 81), | |
| <= 74 and >= 50 => (short)GetRandom(44, 60), | |
| <= 49 and >= 25 => (short)GetRandom(25, 44), | |
| _ => (short)GetRandom(10, 25), | |
| }; | |
| private static short GetLotValueBiome() => (short)GetRandom(20, 101); | |
| private static string GenerateAreaList(int area, bool canSwim) | |
| { | |
| // total number of areas per map | |
| var maxAreas = area switch | |
| { | |
| 0 when !canSwim => 23, | |
| 0 => 27, | |
| 1 => 11, | |
| 2 => 4, | |
| _ => throw new ArgumentException(nameof(area)), | |
| }; | |
| // total number of areas to assign per entry | |
| var maxSelection = area switch | |
| { | |
| 0 => 10, | |
| 1 => 6, | |
| 2 => 2, | |
| _ => throw new ArgumentException(nameof(area)), | |
| }; | |
| var areas = Enumerable.Range(1, maxAreas).ToList(); | |
| ShuffleData(areas); | |
| areas = areas[0..maxSelection].OrderBy(z => z).ToList(); | |
| return string.Join(",", areas); | |
| } | |
| private static EnableTable GetEnableTable(ushort dev, byte form) | |
| { | |
| var species = GetNationalDexID(dev); | |
| var enable = new EnableTable() { Land = true }; | |
| if (GetCanSwimOrBeAboveWater(species, form)) | |
| enable.UpWater = true; | |
| if (GetCanFlyOrLevitate(species, form)) | |
| enable.Air1 = enable.Air2 = true; | |
| return enable; | |
| } | |
| private static TimeTable GetTimeTable() | |
| { | |
| if (!MAKE_WILD_POKEMON_TIME_OF_DAY_DEPENDENT) | |
| return new() { Morning = true, Noon = true, Evening = true, Night = true }; | |
| var rand = GetRandom(2); | |
| var time = new TimeTable(); | |
| if (rand == 0) time.Morning = time.Noon = true; | |
| if (rand == 1) time.Evening = time.Night = true; | |
| return time; | |
| } | |
| private static BandType GetBandType(bool IsBisharp, bool IsEncounterDisguised) | |
| { | |
| // always spawn as a boss to ensure Bisharp is able to evolve | |
| if (!MAKE_EVOLUTIONS_EASIER && IsBisharp) | |
| return BandType.BOSS; | |
| // Ditto, Zorua, and Zoroark can not spawn in bands if they are disguised | |
| if (!REMOVE_DITTO_ZORUA_ZOROARK_WILD_DISGUISES && IsEncounterDisguised) | |
| return BandType.NONE; | |
| return GetRandom(100) switch | |
| { | |
| <= 99 and >= 15 => BandType.BOSS, // 85% | |
| <= 14 and >= 5 => BandType.SAME, // 10% | |
| _ => BandType.NONE, // 5% | |
| }; | |
| } | |
| private static short GetBandRate(BandType type, bool IsBisharp) | |
| { | |
| // not eligible for a band | |
| if (type == BandType.NONE) | |
| return 0; | |
| // always spawn in a band to ensure Bisharp is able to evolve | |
| if (!MAKE_EVOLUTIONS_EASIER && IsBisharp) | |
| return 100; | |
| // evenly split 25% | |
| return GetRandom(100) switch | |
| { | |
| <= 99 and >= 75 => 70, | |
| <= 74 and >= 50 => 50, | |
| <= 49 and >= 25 => 35, | |
| _ => 20, | |
| }; | |
| } | |
| private static string GetPokeVoiceClassification() | |
| { | |
| var voices = new[] { "ANIMAL", "ANIMAL_CARNIVORE", "ANIMAL_LITTLE", "BIRD", "BIRD_LITTLE", "CROWS", "INSECT", "LARGE_CREATURES", "OTHERS" }; | |
| return voices[GetRandom(voices.Length)]; | |
| } | |
| private static string GetBandDisplayText(BandType type, short rate) => type switch | |
| { | |
| BandType.NONE => string.Empty, | |
| BandType.SAME => $"Same ({rate}%)", | |
| BandType.BOSS => $"Boss ({rate}%)", | |
| _ => throw new ArgumentException(nameof(type)), | |
| }; | |
| private static string GetBringItemDisplayText(BringItem item) | |
| { | |
| if (item is { ItemID: ItemID.ITEMID_NONE } or { BringRate: 0 }) | |
| return string.Empty; | |
| return $"{item.BringRate}% {StringsItem[(ushort)item.ItemID]}"; | |
| } | |
| private static string GetTimeTableDisplayText(TimeTable tt) | |
| { | |
| List<string> Result = []; | |
| if (tt.Morning) Result.Add("Morning"); | |
| if (tt.Noon) Result.Add("Day"); | |
| if (tt.Evening) Result.Add("Evening"); | |
| if (tt.Night) Result.Add("Night"); | |
| return string.Join(", ", Result); | |
| } | |
| private static BringItem GetBringItem(DevID species, sbyte form, int area) | |
| { | |
| if (!MAKE_EVOLUTIONS_EASIER && species == DevID.DEV_KIRIKIZAN) | |
| return new BringItem() { ItemID = ItemID.ITEMID_KASIRANOAKASI, BringRate = 100 }; // always give Leader's Crest | |
| if (IsFormHeldItemDependent(species, form)) | |
| return new BringItem() { ItemID = GetFormSpecificItem(species, (short)form), BringRate = 100 }; | |
| if (GIVE_POKEMON_RANDOM_HELD_ITEMS) | |
| return new BringItem() { ItemID = GetRandomItem(), BringRate = GetBringRate() }; | |
| return GetVanillaBringItem(species, form, area); | |
| } | |
| private static sbyte GetBringRate() => (sbyte)GetRandom(100) switch | |
| { | |
| <= 99 and >= 75 => 50, // 25% | |
| <= 74 and >= 50 => 30, // 25% | |
| <= 49 and >= 30 => 20, // 20% | |
| <= 29 and >= 10 => 10, // 20% | |
| _ => 5, // 10% | |
| }; | |
| private static BringItem GetVanillaBringItem(DevID species, sbyte form, int area) => GetNationalDexID((ushort)species) switch | |
| { | |
| 0025 => new BringItem() { ItemID = (ItemID)0236, BringRate = 05 }, // Pikachu: Light Ball (5%) | |
| 0113 => new BringItem() { ItemID = (ItemID)0110, BringRate = 30 }, // Chansey: Oval Stone (30%) | |
| 0116 => new BringItem() { ItemID = (ItemID)0235, BringRate = 05 }, // Horsea: Dragon Scale (5%) | |
| 0117 => new BringItem() { ItemID = (ItemID)0235, BringRate = 10 }, // Seadra: Dragon Scale (10%) | |
| 0216 => new BringItem() { ItemID = (ItemID)0094, BringRate = 05 }, // Teddiursa: Honey (5%) | |
| 0242 => new BringItem() { ItemID = (ItemID)0110, BringRate = 30 }, // Blissey: Oval Stone (30%) | |
| 0283 => new BringItem() { ItemID = (ItemID)0094, BringRate = 05 }, // Surskit: Honey (5%) | |
| 0285 => new BringItem() { ItemID = (ItemID)0086, BringRate = 05 }, // Shroomish: Tiny Mushroom (5%) | |
| 0286 => new BringItem() { ItemID = (ItemID)0086, BringRate = 30 }, // Breloom: Tiny Mushroom (30%) | |
| 0316 => new BringItem() { ItemID = (ItemID)0155, BringRate = 30 }, // Gulpin: Oran Berry (30%) | |
| 0317 => new BringItem() { ItemID = (ItemID)0158, BringRate = 05 }, // Swalot: Sitrus Berry (5%) | |
| 0415 => new BringItem() { ItemID = (ItemID)0094, BringRate = 30 }, // Combee: Honey (30%) | |
| 0440 => new BringItem() { ItemID = (ItemID)0110, BringRate = 05 }, // Happiny: Oval Stone (5%) | |
| 0590 => new BringItem() { ItemID = (ItemID)0086, BringRate = 05 }, // Foongus: Tiny Mushroom (5%) | |
| 0591 => new BringItem() { ItemID = (ItemID)0086, BringRate = 30 }, // Amoonguss: Tiny Mushroom (30%) | |
| 0731 => new BringItem() { ItemID = (ItemID)0155, BringRate = 30 }, // Pikipek: Oran Berry (30%) | |
| 0732 => new BringItem() { ItemID = (ItemID)0158, BringRate = 10 }, // Trumbeak: Sitrus Berry (10%) | |
| 0733 => new BringItem() { ItemID = (ItemID)0158, BringRate = 30 }, // Toucannon: Sitrus Berry (30%) | |
| 0734 => new BringItem() { ItemID = (ItemID)0151, BringRate = 05 }, // Yungoos: Pecha Berry (5%) | |
| 0735 => new BringItem() { ItemID = (ItemID)0151, BringRate = 05 }, // Gumshoos: Pecha Berry (5%) | |
| 0739 => new BringItem() { ItemID = (ItemID)0153, BringRate = 05 }, // Crabrawler: Aspear Berry (5%) | |
| 0740 => new BringItem() { ItemID = (ItemID)0149, BringRate = 05 }, // Crabominable: Cheri Berry (5%) | |
| 0742 => new BringItem() { ItemID = (ItemID)0094, BringRate = 30 }, // Cutiefly: Honey (30%) | |
| 0743 => new BringItem() { ItemID = (ItemID)0094, BringRate = 20 }, // Ribombee: Honey (20%) | |
| 0778 => new BringItem() { ItemID = (ItemID)0150, BringRate = 05 }, // Mimikyu: Chesto Berry (5%) | |
| 0948 => new BringItem() { ItemID = (ItemID)0086, BringRate = 05 }, // Toedscool: Tiny Mushroom (5%) | |
| 0949 => new BringItem() { ItemID = (ItemID)0086, BringRate = 30 }, // Toedscruel: Tiny Mushroom (30%) | |
| 0984 => new BringItem() { ItemID = (ItemID)1880, BringRate = 05 }, // Great Tusk: Booster Energy (5%) | |
| 0985 => new BringItem() { ItemID = (ItemID)1880, BringRate = 05 }, // Scream Tail: Booster Energy (5%) | |
| 0986 => new BringItem() { ItemID = (ItemID)1880, BringRate = 05 }, // Brute Bonnet: Booster Energy (5%) | |
| 0987 => new BringItem() { ItemID = (ItemID)1880, BringRate = 05 }, // Flutter Mane: Booster Energy (5%) | |
| 0988 => new BringItem() { ItemID = (ItemID)1880, BringRate = 05 }, // Slither Wing: Booster Energy (5%) | |
| 0989 => new BringItem() { ItemID = (ItemID)1880, BringRate = 05 }, // Sandy Shocks: Booster Energy (5%) | |
| 0990 => new BringItem() { ItemID = (ItemID)1880, BringRate = 05 }, // Iron Treads: Booster Energy (5%) | |
| 0991 => new BringItem() { ItemID = (ItemID)1880, BringRate = 05 }, // Iron Bundle: Booster Energy (5%) | |
| 0992 => new BringItem() { ItemID = (ItemID)1880, BringRate = 05 }, // Iron Hands: Booster Energy (5%) | |
| 0993 => new BringItem() { ItemID = (ItemID)1880, BringRate = 05 }, // Iron Jugulis: Booster Energy (5%) | |
| 0994 => new BringItem() { ItemID = (ItemID)1880, BringRate = 05 }, // Iron Moth: Booster Energy (5%) | |
| 0995 => new BringItem() { ItemID = (ItemID)1880, BringRate = 05 }, // Iron Thorns: Booster Energy (5%) | |
| 1005 => new BringItem() { ItemID = (ItemID)1880, BringRate = 05 }, // Roaring Moon: Booster Energy (5%) | |
| 1006 => new BringItem() { ItemID = (ItemID)1880, BringRate = 05 }, // Iron Valiant: Booster Energy (5%) | |
| // Paldea, Blueberry Academy | |
| 0819 when area != 1 => new BringItem() { ItemID = (ItemID)0155, BringRate = 05 }, // Skwovet: Oran Berry (5%) | |
| 0820 when area != 1 => new BringItem() { ItemID = (ItemID)0158, BringRate = 05 }, // Greedent: Sitrus Berry (5%) | |
| // Kitakami | |
| 0819 when area == 1 => new BringItem() { ItemID = (ItemID)0154, BringRate = 05 }, // Skwovet: Leppa Berry (5%) | |
| 0820 when area == 1 => new BringItem() { ItemID = (ItemID)0154, BringRate = 10 }, // Greedent: Leppa Berry (10%) | |
| // Oricorio | |
| 0741 when form == 0 => new BringItem() { ItemID = (ItemID)0853, BringRate = 05 }, // Baile: Red Nectar (5%) | |
| 0741 when form == 1 => new BringItem() { ItemID = (ItemID)0854, BringRate = 05 }, // Pom-Pom: Yellow Nectar (5%) | |
| 0741 when form == 2 => new BringItem() { ItemID = (ItemID)0855, BringRate = 05 }, // Pa'u: Pink Nectar (5%) | |
| 0741 when form == 3 => new BringItem() { ItemID = (ItemID)0856, BringRate = 05 }, // Sensu: Purple Nectar (5%) | |
| // Minior (Meteor Form and matching Core) | |
| 0774 when form is 0 or 7 => new BringItem() { ItemID = (ItemID)0238, BringRate = 30 }, // Red: Hard Stone (30%) | |
| 0774 when form is 1 or 8 => new BringItem() { ItemID = (ItemID)0238, BringRate = 30 }, // Orange: Hard Stone (30%) | |
| 0774 when form is 2 or 9 => new BringItem() { ItemID = (ItemID)0539, BringRate = 40 }, // Yellow: Float Stone (40%) | |
| 0774 when form is 3 or 10 => new BringItem() { ItemID = (ItemID)0539, BringRate = 40 }, // Green: Float Stone (40%) | |
| 0774 when form is 4 or 11 => new BringItem() { ItemID = (ItemID)0238, BringRate = 30 }, // Blue: Hard Stone (30%) | |
| 0774 when form is 5 or 12 => new BringItem() { ItemID = (ItemID)0238, BringRate = 30 }, // Indigo: Hard Stone (30%) | |
| 0774 when form is 6 or 13 => new BringItem() { ItemID = (ItemID)0539, BringRate = 40 }, // Violet: Float Stone (40%) | |
| _ => new BringItem(), | |
| }; | |
| private static List<Biome> GetRandomBiomes(int max, string areas, bool canSwim) | |
| { | |
| var biomes = Enumerable.Range(1, max).Select(z => (Biome)z).ToList(); | |
| ShuffleData(biomes); | |
| // don't allow water biomes if it can't swim | |
| if (!canSwim) | |
| { | |
| biomes.Remove(Biome.LAKE); | |
| biomes.Remove(Biome.RIVER); | |
| biomes.Remove(Biome.OSEAN); | |
| biomes.Remove(Biome.CAVE_WATER); | |
| } | |
| // Paldean Sea spawners are entirely water, so force an Ocean biome if it can swim and is assigned one of the area IDs | |
| if (canSwim && !biomes[..4].Contains(Biome.OSEAN) && (areas.Contains("24") || areas.Contains("25") || areas.Contains("26") || areas.Contains("27"))) | |
| biomes[0] = Biome.OSEAN; | |
| return biomes[..4]; | |
| } | |
| private static List<Biome> GetAreaZeroBiomes() | |
| { | |
| List<Biome> Biomes = [ Biome.GRASS, Biome.RIVER, Biome.UNDERGROUND, Biome.ROCKY, Biome.CAVE ]; | |
| ShuffleData(Biomes); | |
| return Biomes[..4]; | |
| } | |
| private static void ModifyWildPokemonLevels() | |
| { | |
| var fieldAreaData = new[] | |
| { | |
| // Paldea | |
| "world/data/field/area/field_dungeon_area/field_dungeon_area_array.bin", | |
| "world/data/field/area/field_inside_area/field_inside_area_array.bin", | |
| "world/data/field/area/field_location/field_location_array.bin", | |
| "world/data/field/area/field_main_area/field_main_area_array.bin", | |
| "world/data/field/area/field_sub_area/field_sub_area_array.bin", | |
| // Kitakami | |
| "world/data/field/area_su1/field_dungeon_area_su1/field_dungeon_area_su1_array.bin", | |
| "world/data/field/area_su1/field_inside_area_su1/field_inside_area_su1_array.bin", | |
| "world/data/field/area_su1/field_location_su1/field_location_su1_array.bin", | |
| "world/data/field/area_su1/field_main_area_su1/field_main_area_su1_array.bin", | |
| "world/data/field/area_su1/field_sub_area_su1/field_sub_area_su1_array.bin", | |
| // Blueberry Academy | |
| "world/data/field/area_su2/field_dungeon_area_su2/field_dungeon_area_su2_array.bin", | |
| "world/data/field/area_su2/field_inside_area_su2/field_inside_area_su2_array.bin", | |
| "world/data/field/area_su2/field_main_area_su2/field_main_area_su2_array.bin", | |
| "world/data/field/area_su2/field_sub_area_su2/field_sub_area_su2_array.bin", | |
| }; | |
| var pointData = new[] | |
| { | |
| "world/data/encount/point_data/point_data/encount_data_100000.bin", | |
| "world/data/encount/point_data/point_data/encount_data_atlantis.bin", | |
| "world/data/encount/point_data/point_data/encount_data_su1.bin", | |
| "world/data/encount/point_data/point_data/encount_data_su2.bin", | |
| }; | |
| var outbreakPointData = new[] | |
| { | |
| "world/data/encount/point_data/outbreak_point_data/outbreak_point_main.bin", | |
| "world/data/encount/point_data/outbreak_point_data/outbreak_point_su1.bin", | |
| "world/data/encount/point_data/outbreak_point_data/outbreak_point_su2.bin", | |
| }; | |
| // general areas | |
| foreach (var a in fieldAreaData) | |
| { | |
| var index = a.LastIndexOf("/") + 1; | |
| var f = a[index..]; | |
| var d = a[..index]; | |
| var file = Path.Combine(PATH_MASTER, d, f); | |
| var obj = FlatBufferConverter.DeserializeFrom<FieldMainAreaArray>(file); // schema doesn't actually matter, since they all use an identical schema with AreaInfo objects | |
| foreach (var t in obj.Table.Where(z => z.Info.MinEncLv != 0 && z.Info.MaxEncLv != 0)) | |
| { | |
| t.Info.MinEncLv = GetModifiedLevel(t.Info.MinEncLv); | |
| t.Info.MaxEncLv = GetModifiedLevel(t.Info.MaxEncLv); | |
| } | |
| var data = FlatBufferConverter.SerializeFrom(obj); | |
| var dest = Path.Combine(DEST_MASTER, d); | |
| if (!Directory.Exists(dest)) | |
| Directory.CreateDirectory(dest); | |
| File.WriteAllBytes(Path.Combine(dest, f), data); | |
| } | |
| // individual spawners | |
| foreach (var e in pointData) | |
| { | |
| var index = e.LastIndexOf("/") + 1; | |
| var f = e[index..]; | |
| var d = e[..index]; | |
| var file = Path.Combine(PATH_MASTER, d, f); | |
| var obj = FlatBufferConverter.DeserializeFrom<PointDataArray>(file); | |
| List<PointData> PointDataEntries = []; | |
| foreach (var t in obj.Table) | |
| { | |
| var min = GetModifiedLevel((int)t.LevelRange.X); | |
| var max = GetModifiedLevel((int)t.LevelRange.Y); | |
| var pd = new PointData | |
| { | |
| Position = t.Position, | |
| LevelRange = new PackedVec2f { X = min, Y = max }, | |
| Biome = t.Biome, | |
| Substance = t.Substance, | |
| AreaNo = t.AreaNo, | |
| }; | |
| PointDataEntries.Add(pd); | |
| } | |
| obj.Table.Clear(); | |
| foreach (var t in PointDataEntries) | |
| obj.Table.Add(t); | |
| var data = FlatBufferConverter.SerializeFrom(obj); | |
| var dest = Path.Combine(DEST_MASTER, d); | |
| if (!Directory.Exists(dest)) | |
| Directory.CreateDirectory(dest); | |
| File.WriteAllBytes(Path.Combine(dest, f), data); | |
| } | |
| // outbreak spawners | |
| foreach (var e in outbreakPointData) | |
| { | |
| var index = e.LastIndexOf("/") + 1; | |
| var f = e[index..]; | |
| var d = e[..index]; | |
| var file = Path.Combine(PATH_MASTER, d, f); | |
| var obj = FlatBufferConverter.DeserializeFrom<OutbreakPointArray>(file); | |
| List<OutbreakPointData> PointDataEntries = []; | |
| foreach (var t in obj.Table) | |
| { | |
| var min = GetModifiedLevel((int)t.LevelRange.X); | |
| var max = GetModifiedLevel((int)t.LevelRange.Y); | |
| var pd = new OutbreakPointData | |
| { | |
| Position = t.Position, | |
| LevelRange = new PackedVec2f { X = min, Y = max }, | |
| IBiome = t.IBiome, | |
| ISubstance = t.ISubstance, | |
| AreaNo = t.AreaNo, | |
| AreaName = t.AreaName, | |
| EnableTable = t.EnableTable, | |
| }; | |
| PointDataEntries.Add(pd); | |
| } | |
| obj.Table.Clear(); | |
| foreach (var t in PointDataEntries) | |
| obj.Table.Add(t); | |
| var data = FlatBufferConverter.SerializeFrom(obj); | |
| var dest = Path.Combine(DEST_MASTER, d); | |
| if (!Directory.Exists(dest)) | |
| Directory.CreateDirectory(dest); | |
| File.WriteAllBytes(Path.Combine(dest, f), data); | |
| } | |
| } | |
| private static void RandomizeFixedSymbolEncounters() | |
| { | |
| "Randomizing fixed symbol encounters...".Dump(); | |
| const string file = "fixed_symbol_table_array.bin"; | |
| var dest = Path.Combine(DEST_MASTER, "world/data/field/fixed_symbol/fixed_symbol_table/"); | |
| var path = Path.Combine(PATH_MASTER, "world/data/field/fixed_symbol/fixed_symbol_table/"); | |
| var obj = FlatBufferConverter.DeserializeFrom<FixedSymbolTableArray>(Path.Combine(path, file)); | |
| List<FixedSymbolTable> NewSpecies = []; | |
| bool ShouldRandomizeEncounterAgain(string key, ushort species, short form) => (IsFixedSymbolFlying(key) && !GetCanFlyOrLevitate(species, form)) || (IsFixedSymbolSwimming(key) && !GetCanSwimOrBeAboveWater(species, form)); | |
| bool ShouldTweakBehaviorAction(PokemonActionID action, DevID dev, GemType gem) | |
| { | |
| // Pokémon that normally don't have behavior data | |
| if (action == PokemonActionID.NONE && MissingBehaviorDataSingle.Contains($"{(ushort)dev:0000}_00_00")) | |
| { | |
| // always fixed to a certain Tera Type | |
| if (dev is DevID.DEV_KAMENONI or DevID.DEV_KODAIGAME) | |
| return true; | |
| // only apply to encounters that are not wild Tera Pokémon | |
| return gem == GemType.DEFAULT; | |
| } | |
| // Greavard and Houndstone typically pop out of the ground, while other Pokémon with this behavior will instead just stand completely still | |
| if (action == PokemonActionID.OBAKEINU_GROUND_WAIT && dev is not (DevID.DEV_OBAKEINU1 or DevID.DEV_OBAKEINU2)) | |
| return true; | |
| return false; | |
| } | |
| foreach (var enc in obj.Table) | |
| { | |
| var t = enc.Symbol; | |
| var key = enc.TableKey; | |
| var ai = enc.PokeAI; | |
| var ctr = 0; | |
| if (RANDOMIZE_POKEMON_TO_HAVE_SIMILAR_STRENGTH) | |
| { | |
| var pk = GetSimilarStrengthPokemon(t.DevId, t.FormId); | |
| t.DevId = pk.Species; | |
| t.FormId = pk.Form; | |
| } | |
| else | |
| { | |
| t.DevId = GetRandomSpecies(t.DevId); | |
| t.FormId = GetRandomForm(t.DevId); | |
| } | |
| if (MODIFY_POKEMON_LEVELS) | |
| t.Level = GetModifiedLevel(t.Level); | |
| if (FORCE_FULLY_EVOLVED_POKEMON && t.Level >= FORCE_FULLY_EVOLVED_POKEMON_LEVEL) | |
| { | |
| var pk = ForceEvolveSpeciesFully(t.DevId, t.FormId); | |
| t.DevId = pk.Species; | |
| t.FormId = pk.Form; | |
| } | |
| // spawn coordinates are fixed, so we need to properly swap flying spawns and water spawns with other species capable of flying or swimming | |
| while (ShouldRandomizeEncounterAgain(key, GetNationalDexID((ushort)t.DevId), t.FormId) && ctr <= 100) | |
| { | |
| ctr++; | |
| t.DevId = GetRandomSpecies(0); | |
| t.FormId = GetRandomForm(t.DevId); | |
| if (FORCE_FULLY_EVOLVED_POKEMON && t.Level >= FORCE_FULLY_EVOLVED_POKEMON_LEVEL) | |
| { | |
| var pk = ForceEvolveSpeciesFully(t.DevId, t.FormId); | |
| t.DevId = pk.Species; | |
| t.FormId = pk.Form; | |
| } | |
| } | |
| // no item in schema, force base form | |
| if (IsFormHeldItemDependent(t.DevId, t.FormId)) | |
| t.FormId = 0; | |
| t.Sex = SexType.DEFAULT; | |
| t.RareType = RareType.DEFAULT; | |
| t.WazaType = WazaType.DEFAULT; | |
| t.Waza1 = GetCleanMoveSlot(); | |
| t.Waza2 = GetCleanMoveSlot(); | |
| t.Waza3 = GetCleanMoveSlot(); | |
| t.Waza4 = GetCleanMoveSlot(); | |
| t.TokuseiIndex = GetRandomAbilitySetter(); | |
| // keep valid for Ogerpon or Terapagos, preserve Stellar, random if wild Tera, otherwise randomize according to setting | |
| t.GemType = t.GemType switch | |
| { | |
| _ when t.DevId is DevID.DEV_KAMENONI or DevID.DEV_KODAIGAME => GetTeraType(t.DevId, t.FormId), | |
| GemType.NIJI => GemType.NIJI, | |
| not (GemType.DEFAULT or GemType.RANDOM) => (GemType)GetRandom(2, 20), | |
| _ => GetTeraType(t.DevId, t.FormId), | |
| }; | |
| if (t.DevId == DevID.DEV_MAHOIPPU && key != "common_0150_HERUGAA") | |
| t.MahoippuViewId = GetRandomAlcremieTopping(); | |
| if (GIVE_ALL_POKEMON_MAX_INDIVIDUAL_VALUES) | |
| { | |
| t.TalentType = TalentType.VALUE; | |
| t.TalentValue = FlawlessIVSpread; | |
| t.TalentVNum = 0; | |
| } | |
| // tweak behavior to be more fluid | |
| if (ShouldTweakBehaviorAction(ai.ActionId, t.DevId, t.GemType)) | |
| ai.ActionId = PokemonActionID.EXCLUSION_AREA_ESCAPE_RUN_IF_BATTLE_MOVE_TO_NEARBY_BATTLE_HECKLER_AREA; | |
| NewSpecies.Add(enc); | |
| } | |
| if (GENERATE_SPOILER_LOG_FILE) | |
| WriteFixedSymbolChangesToLog(NewSpecies); | |
| if (!Directory.Exists(dest)) | |
| Directory.CreateDirectory(dest); | |
| var data = FlatBufferConverter.SerializeFrom(obj); | |
| File.WriteAllBytes(Path.Combine(dest, file), data); | |
| } | |
| private static List<FixedSymbolTable> GetOldFixedSymbolEncounters() | |
| { | |
| const string file = "fixed_symbol_table_array.bin"; | |
| var path = Path.Combine(PATH_MASTER, "world/data/field/fixed_symbol/fixed_symbol_table/"); | |
| var obj = FlatBufferConverter.DeserializeFrom<FixedSymbolTableArray>(Path.Combine(path, file)); | |
| return obj.Table.Select(z => z).ToList(); | |
| } | |
| private static void RandomizeStaticEncounters() | |
| { | |
| "Randomizing static encounters...".Dump(); | |
| List<EventBattlePokemon> NewSpecies = []; | |
| List<string> Blacklist = []; | |
| const string file = "eventBattlePokemon_array.bin"; | |
| var dest = Path.Combine(DEST_MASTER, "world/data/battle/eventBattlePokemon/"); | |
| var path = Path.Combine(PATH_MASTER, "world/data/battle/eventBattlePokemon/"); | |
| var obj = FlatBufferConverter.DeserializeFrom<EventBattlePokemonArray>(Path.Combine(path, file)); | |
| var table = obj.Table; | |
| if (!RANDOMIZE_KORAIDON_MIRAIDON) | |
| { | |
| Blacklist.Add(table[031].Label); | |
| Blacklist.Add(table[032].Label); | |
| Blacklist.Add(table[115].Label); | |
| Blacklist.Add(table[116].Label); | |
| } | |
| byte GetAppropriateType(string id) => id switch | |
| { | |
| "gym_kusa_020_KIMAWARI_03" => ThemedTypes[01], // Sunflora | |
| "nusi_962_02" => ThemedTypes[13], // Klawf | |
| "nusi_959_02" => ThemedTypes[14], // Bombirdier | |
| "nusi_944_02" => ThemedTypes[15], // Orthworm | |
| "nusi_986_02" => ThemedTypes[16], // Great Tusk / Iron Treads | |
| "nusi_952_01" or "nusi_931_02" => ThemedTypes[17], // Tatsugiri / Dondozo | |
| _ => throw new ArgumentException(nameof(id)), | |
| }; | |
| bool IsSpecificBoss(string id) => !id.StartsWith("coin_") && !id.Contains("_dummy") && !id.Contains("semi_") && !id.Contains("su2_") && !id.Contains("SDC02_sub"); | |
| bool IsTitan(string id) => id is "nusi_962_02" or "nusi_959_02" or "nusi_944_02" or "nusi_986_02" or "nusi_952_01" or "nusi_931_02"; | |
| bool IsTypeThemedEncounter(string id) => id == "gym_kusa_020_KIMAWARI_03" || IsTitan(id); | |
| bool ShouldRandomizeEncounterAgain(string id, DevID species, short form, int level, (byte Type1, byte Type2) Types) | |
| { | |
| // phase 4 against Ogerpon requires Tera Grass to prevent a softlock, so if Tera Types must be default, force a Grass type | |
| if (id == "SDC01_kamenoni_4_strong" && !ALLOW_FULLY_RANDOM_TERA_TYPES && !(Types.Type1 == (byte)TypeIndex.Grass || Types.Type2 == (byte)TypeIndex.Grass)) | |
| return true; | |
| // Ogerpon encounter should not be randomized into a Pokémon that requires a specific Tera Type | |
| if (id == "SDC01_kamenoni_4_strong" && species is DevID.DEV_KAMENONI or DevID.DEV_KODAIGAME) | |
| return true; | |
| // ensure Dondozo and Tatsugiri randomize into unique species | |
| if (id == "nusi_952_01" && species == obj.Table[034].PokeData.DevId) | |
| return true; | |
| // evolution may break the type theme | |
| if (TYPE_THEMED_IMPORTANT_TRAINERS != TypeThemeRandomizer.NotTypeThemed && IsTitan(id) && ((FORCE_FULLY_EVOLVED_POKEMON && level >= FORCE_FULLY_EVOLVED_POKEMON_LEVEL) || FORCE_FULLY_EVOLVED_TITAN_POKEMON) && !(Types.Type1 == GetAppropriateType(id) || Types.Type2 == GetAppropriateType(id))) | |
| return true; | |
| // model does not disappear after first phase, just disallow it | |
| if (IsTitan(id) && HasBattleOnlyForm(GetNationalDexID((ushort)species))) | |
| return true; | |
| // softlock prevention | |
| if (GetIsInvalidScenePokemon((ushort)species)) | |
| return true; | |
| return false; | |
| } | |
| foreach (var enc in table.Where(z => !Blacklist.Contains(z.Label))) | |
| { | |
| var t = enc.PokeData; | |
| var key = enc.Label; | |
| var ctr = 0; | |
| if (RANDOMIZE_POKEMON_TO_HAVE_SIMILAR_STRENGTH) | |
| { | |
| var pk = GetSimilarStrengthPokemon(t.DevId, t.FormId); | |
| t.DevId = pk.Species; | |
| t.FormId = pk.Form; | |
| } | |
| else | |
| { | |
| t.DevId = GetRandomSpecies(t.DevId); | |
| t.FormId = GetRandomForm(t.DevId); | |
| } | |
| // make Houndoom match fixed symbol encounter | |
| if (key == "common_0150_HERUGAA") | |
| { | |
| var fixedSymbols = Path.Combine(DEST_MASTER, "world/data/field/fixed_symbol/fixed_symbol_table/fixed_symbol_table_array.bin"); | |
| var tableF = FlatBufferConverter.DeserializeFrom<FixedSymbolTableArray>(fixedSymbols).Table; | |
| var houndoom = tableF.FirstOrDefault(z => z.TableKey == "ai_event150_05").Symbol; | |
| t.DevId = houndoom.DevId; | |
| t.FormId = houndoom.FormId; | |
| t.Sex = houndoom.Sex; | |
| t.RareType = houndoom.RareType; | |
| } | |
| // set Sunflora and Titans to match a type theme | |
| if (TYPE_THEMED_IMPORTANT_TRAINERS != TypeThemeRandomizer.NotTypeThemed && IsTypeThemedEncounter(key)) | |
| { | |
| var type = GetAppropriateType(key); | |
| var pk = GetTypeThemedPokemon(type); | |
| t.DevId = pk.Species; | |
| t.FormId = pk.Form; | |
| } | |
| var types = GetPokemonTypes(t.DevId, t.FormId); | |
| t.Sex = IsSpecificBoss(key) || GetHasGenderDifference((ushort)t.DevId, t.FormId) ? GetRandomGender(t.DevId, t.FormId) : SexType.DEFAULT; | |
| t.Seikaku = t.SeikakuHosei = IsSpecificBoss(key) ? GetRandomNature(t.DevId, t.FormId) : SeikakuType.DEFAULT; | |
| t.RareType = IsSpecificBoss(key) ? RollForRandomShiny() : RareType.DEFAULT; | |
| if (GIVE_OPPONENT_SMEARGLE_RANDOM_MOVESETS && IsSpecificBoss(key) && t.DevId == DevID.DEV_DOOBURU) | |
| { | |
| var moves = GetRandomSmeargleMoveset(); | |
| t.WazaType = WazaType.MANUAL; | |
| t.Waza1.WazaId = moves[0]; | |
| t.Waza2.WazaId = moves[1]; | |
| t.Waza3.WazaId = moves[2]; | |
| t.Waza4.WazaId = moves[3]; | |
| } | |
| else | |
| { | |
| t.WazaType = WazaType.DEFAULT; | |
| t.Waza1 = GetCleanMoveSlot(); | |
| t.Waza2 = GetCleanMoveSlot(); | |
| t.Waza3 = GetCleanMoveSlot(); | |
| t.Waza4 = GetCleanMoveSlot(); | |
| } | |
| t.Tokusei = IsSpecificBoss(key) ? GetRandomAbilitySlot() : GetRandomAbilitySetter(); | |
| t.DropItem = GetRandomItem(); | |
| t.DropItemNum = 1; | |
| if (MODIFY_POKEMON_LEVELS) | |
| t.Level = GetModifiedLevel(t.Level); | |
| if ((FORCE_FULLY_EVOLVED_POKEMON && t.Level >= FORCE_FULLY_EVOLVED_POKEMON_LEVEL) || (FORCE_FULLY_EVOLVED_TITAN_POKEMON && IsTitan(key))) | |
| EvolveStaticEncounter(t, IsSpecificBoss(key)); | |
| // re-randomize if the encounter requires it | |
| while (ShouldRandomizeEncounterAgain(key, t.DevId, t.FormId, t.Level, types) && ctr <= 100) | |
| { | |
| ctr++; | |
| if (TYPE_THEMED_IMPORTANT_TRAINERS != TypeThemeRandomizer.NotTypeThemed && IsTypeThemedEncounter(key)) | |
| { | |
| var type = GetAppropriateType(key); | |
| var pk = GetTypeThemedPokemon(type); | |
| t.DevId = pk.Species; | |
| t.FormId = pk.Form; | |
| if ((FORCE_FULLY_EVOLVED_POKEMON && t.Level >= FORCE_FULLY_EVOLVED_POKEMON_LEVEL) || (FORCE_FULLY_EVOLVED_TITAN_POKEMON && IsTitan(key))) | |
| EvolveStaticEncounter(t, IsSpecificBoss(key)); | |
| types = GetPokemonTypes(t.DevId, t.FormId); // refresh | |
| } | |
| else | |
| { | |
| t.DevId = GetRandomSpecies(t.DevId); | |
| t.FormId = GetRandomForm(t.DevId); | |
| if ((FORCE_FULLY_EVOLVED_POKEMON && t.Level >= FORCE_FULLY_EVOLVED_POKEMON_LEVEL) || (FORCE_FULLY_EVOLVED_TITAN_POKEMON && IsTitan(key))) | |
| EvolveStaticEncounter(t, IsSpecificBoss(key)); | |
| types = GetPokemonTypes(t.DevId, t.FormId); // refresh | |
| } | |
| } | |
| // keep Area Zero Underdepths Stellars as Stellar, handle Ogerpon edge case, otherwise, pick fitting Tera Type | |
| t.GemType = t.GemType == GemType.NIJI ? GemType.NIJI : key switch | |
| { | |
| "SDC01_kamenoni_1" or "SDC01_kamenoni_1_strong" => GemType.HONOO, | |
| "SDC01_kamenoni_2" or "SDC01_kamenoni_2_strong" => GemType.MIZU, | |
| "SDC01_kamenoni_3" or "SDC01_kamenoni_3_strong" => GemType.IWA, | |
| "SDC01_kamenoni_4" or "SDC01_kamenoni_4_strong" => GemType.KUSA, | |
| _ => GetTeraType(t.DevId, t.FormId, IsSpecificBoss(key)), | |
| }; | |
| if (GIVE_POKEMON_RANDOM_RIBBONS_OR_MARKS) | |
| t.SetRibbon = GetRandomRibbonOrMark(); | |
| if (GIVE_POKEMON_RANDOM_HELD_ITEMS) | |
| t.Item = GetRandomItem(); | |
| if (IsFormHeldItemDependent(t.DevId, t.FormId)) | |
| t.Item = GetFormSpecificItem(t.DevId, t.FormId); | |
| if (key == "sub018_BIGUANA") | |
| ApplySoftlockPreventionParadiseProtectionProtocol(t); | |
| if (key == "SDC02_0330_kodaikame") | |
| ApplySoftlockPreventionHiddenTreasureOfAreaZero(t); | |
| if (GIVE_ALL_POKEMON_MAX_INDIVIDUAL_VALUES) | |
| { | |
| t.TalentType = TalentType.VALUE; | |
| t.TalentValue = FlawlessIVSpread; | |
| t.TalentVnum = 0; | |
| } | |
| if (GIVE_BOSS_POKEMON_SMART_HELD_ITEMS && IsSpecificBoss(key) && key is not ("sub018_BIGUANA" or "SDC02_0330_kodaikame")) | |
| { | |
| var pk = new PokeDataBattle() | |
| { | |
| DevId = t.DevId, | |
| FormId = t.FormId, | |
| Sex = t.Sex, | |
| Level = t.Level, | |
| Waza1 = GetCleanMoveSlot(), | |
| Waza2 = GetCleanMoveSlot(), | |
| Waza3 = GetCleanMoveSlot(), | |
| Waza4 = GetCleanMoveSlot(), | |
| Seikaku = t.Seikaku, | |
| Tokusei = t.Tokusei, | |
| TalentValue = t.TalentValue, | |
| EffortValue = t.EffortValue, | |
| }; | |
| t.Item = GetTrainerPokemonHeldItem(pk, false, 1, BattleType.SINGLE); | |
| } | |
| NewSpecies.Add(enc); | |
| } | |
| StandardizeStaticEncounters(obj.Table); | |
| if (!Directory.Exists(dest)) | |
| Directory.CreateDirectory(dest); | |
| var data = FlatBufferConverter.SerializeFrom(obj); | |
| File.WriteAllBytes(Path.Combine(dest, file), data); | |
| UpdateBossText(obj.Table); | |
| UpdateAllStaticScenes(obj.Table); | |
| UpdateAllKitakamiScenes(obj.Table); | |
| if (GENERATE_SPOILER_LOG_FILE) | |
| WriteStaticChangesToLog(NewSpecies); | |
| } | |
| private static void EvolveStaticEncounter(PokeDataEventBattle t, bool specific) | |
| { | |
| var pk = ForceEvolveSpeciesFully(t.DevId, t.FormId); | |
| t.DevId = pk.Species; | |
| t.FormId = pk.Form; | |
| t.Sex = specific || GetHasGenderDifference((ushort)t.DevId, (byte)t.FormId) ? GetRandomGender(t.DevId, t.FormId) : SexType.DEFAULT; // refresh | |
| // refresh Toxtricity nature | |
| if (t.DevId == DevID.DEV_SUTORINDAA && specific) | |
| t.Seikaku = GetRandomNature(t.DevId, t.FormId); | |
| } | |
| private static List<EventBattlePokemon> GetOldStaticEncounters() | |
| { | |
| List<string> Blacklist = []; | |
| const string file = "eventBattlePokemon_array.bin"; | |
| var path = Path.Combine(PATH_MASTER, "world/data/battle/eventBattlePokemon/"); | |
| var obj = FlatBufferConverter.DeserializeFrom<EventBattlePokemonArray>(Path.Combine(path, file)); | |
| if (!RANDOMIZE_KORAIDON_MIRAIDON) | |
| { | |
| Blacklist.Add(obj.Table[031].Label); | |
| Blacklist.Add(obj.Table[032].Label); | |
| Blacklist.Add(obj.Table[115].Label); | |
| Blacklist.Add(obj.Table[116].Label); | |
| } | |
| return obj.Table.Where(z => !Blacklist.Contains(z.Label)).Select(z => z).ToList(); | |
| } | |
| private static void RandomizeGiftEncounters() | |
| { | |
| "Randomizing gift encounters...".Dump(); | |
| const string file = "eventAddPokemon_array.bin"; | |
| var dest = Path.Combine(DEST_MASTER, "world/data/event/event_add_pokemon/eventAddPokemon/"); | |
| var path = Path.Combine(PATH_MASTER, "world/data/event/event_add_pokemon/eventAddPokemon/"); | |
| var obj = FlatBufferConverter.DeserializeFrom<EventAddPokemonArray>(Path.Combine(path, file)); | |
| List<string> Blacklist = [ "add_0195", "add_0193" ]; | |
| List<EventAddPokemon> NewSpecies = []; | |
| bool IsStarterPaldea(string id) => id is "common_0065_hono" or "common_0065_kusa" or "common_0065_mizu"; | |
| bool IsStarterSinnoh(string id) => id is "s1_sub_001_egg_000" or "s1_sub_001_egg_001" or "s1_sub_001_egg_002"; | |
| bool IsSceneEncounter(string id) => IsStarterPaldea(id) || id == "s1_side02_0060_hisuiGardie"; | |
| bool ShouldRandomizeEncounterAgain(string id, DevID dev, short form) | |
| { | |
| if (ENSURE_STARTERS_HAVE_TWO_EVOLUTIONS && !GetIsThreeStageFamily(GetNationalDexID((ushort)dev)) && (IsStarterPaldea(id) || IsStarterSinnoh(id))) | |
| return true; | |
| if (GetIsInvalidScenePokemon((ushort)dev) && IsSceneEncounter(id)) | |
| return true; | |
| if (CurrentlyAssignedStarters.Contains((dev, form))) | |
| return true; | |
| return false; | |
| }; | |
| if (!RANDOMIZE_KORAIDON_MIRAIDON) | |
| { | |
| Blacklist.Add(obj.Table[005].Label); | |
| Blacklist.Add(obj.Table[006].Label); | |
| } | |
| foreach (var enc in obj.Table.Where(z => !Blacklist.Contains(z.Label))) | |
| { | |
| var t = enc.PokeData; | |
| var key = enc.Label; | |
| if (RANDOMIZE_POKEMON_TO_HAVE_SIMILAR_STRENGTH) | |
| { | |
| var pk = GetSimilarStrengthPokemon(t.DevId, t.FormId); | |
| t.DevId = pk.Species; | |
| t.FormId = pk.Form; | |
| } | |
| else | |
| { | |
| t.DevId = GetRandomSpecies(t.DevId); | |
| t.FormId = GetRandomForm(t.DevId); | |
| } | |
| if (RANDOMIZE_STARTERS_INTO_OTHER_STARTERS) | |
| { | |
| if (IsStarterPaldea(key)) | |
| { | |
| var eligible = key switch | |
| { | |
| "common_0065_kusa" => StartersGrass.Except(SpeciesBanlist.Select(z => (ushort)z)).ToList(), | |
| "common_0065_hono" => StartersFire.Except(SpeciesBanlist.Select(z => (ushort)z)).ToList(), | |
| "common_0065_mizu" => StartersWater.Except(SpeciesBanlist.Select(z => (ushort)z)).ToList(), | |
| _ => throw new ArgumentException(nameof(key), key), | |
| }; | |
| t.DevId = (DevID)eligible[GetRandom(eligible.Count)]; | |
| t.FormId = 0; | |
| } | |
| if (IsStarterSinnoh(key)) | |
| { | |
| var eligible = key switch | |
| { | |
| "s1_sub_001_egg_000" => StartersGrass.Except(SpeciesBanlist.Select(z => (ushort)z)).Where(z => z != (ushort)obj.Table[001].PokeData.DevId).ToList(), | |
| "s1_sub_001_egg_001" => StartersFire.Except(SpeciesBanlist.Select(z => (ushort)z)).Where(z => z != (ushort)obj.Table[000].PokeData.DevId).ToList(), | |
| "s1_sub_001_egg_002" => StartersWater.Except(SpeciesBanlist.Select(z => (ushort)z)).Where(z => z != (ushort)obj.Table[002].PokeData.DevId).ToList(), | |
| _ => throw new ArgumentException(nameof(key), key), | |
| }; | |
| // only follow this logic if the pool is large enough | |
| if (eligible.Count >= 1) | |
| { | |
| t.DevId = (DevID)eligible[GetRandom(eligible.Count)]; | |
| t.FormId = 0; | |
| } | |
| } | |
| } | |
| while (ShouldRandomizeEncounterAgain(key, t.DevId, t.FormId)) | |
| { | |
| t.DevId = GetRandomSpecies(0); | |
| t.FormId = GetRandomForm(t.DevId); | |
| } | |
| t.RareType = t.RareType == RareType.RARE ? RareType.RARE : RareType.DEFAULT; | |
| t.Sex = GetHasGenderDifference((ushort)t.DevId, t.FormId) ? GetRandomGender(t.DevId, t.FormId) : SexType.DEFAULT; | |
| t.Tokusei = GetRandomAbilitySetter(); | |
| t.Seikaku = t.SeikakuHosei = SeikakuType.DEFAULT; | |
| t.GemType = GetTeraType(t.DevId, t.FormId); | |
| if (t.Friendship != 255) | |
| t.Friendship = GetBaseFriendship((ushort)t.DevId, (byte)t.FormId); | |
| if (GIVE_POKEMON_RANDOM_POKE_BALLS) | |
| t.BallId = GetRandomBall(); | |
| // apply specified criteria | |
| if (IsStarterPaldea(key)) | |
| { | |
| var blank = new StarterPokemonCriteria(); | |
| var criteria = key switch | |
| { | |
| "common_0065_kusa" => StarterCriteriaGrass, | |
| "common_0065_hono" => StarterCriteriaFire, | |
| "common_0065_mizu" => StarterCriteriaWater, | |
| _ => throw new ArgumentException(nameof(key), key), | |
| }; | |
| if (!criteria.Equals(blank)) | |
| ApplyStarterPokemonCriteria(t, criteria); | |
| } | |
| t.WazaType = WazaType.DEFAULT; | |
| t.Waza1 = GetCleanMoveSlot(); | |
| t.Waza2 = GetCleanMoveSlot(); | |
| t.Waza3 = GetCleanMoveSlot(); | |
| t.Waza4 = GetCleanMoveSlot(); | |
| if (GIVE_POKEMON_RANDOM_RIBBONS_OR_MARKS) | |
| t.SetRibbon = GetRandomRibbonOrMark(); | |
| if (MODIFY_POKEMON_LEVELS && !IsStarterSinnoh(key)) | |
| t.Level = GetModifiedLevel(t.Level); | |
| if (GIVE_POKEMON_RANDOM_HELD_ITEMS) | |
| t.Item = GetRandomItem(); | |
| if (IsFormHeldItemDependent(t.DevId, t.FormId)) | |
| t.Item = GetFormSpecificItem(t.DevId, t.FormId); | |
| if (GIVE_ALL_POKEMON_MAX_INDIVIDUAL_VALUES) | |
| { | |
| t.TalentType = TalentType.VALUE; | |
| t.TalentValue = FlawlessIVSpread; | |
| t.TalentVnum = 0; | |
| } | |
| NewSpecies.Add(enc); | |
| if (IsStarterPaldea(key)) | |
| UpdateStarterBanlist(t.DevId, t.FormId); | |
| } | |
| if (RANDOMIZE_KORAIDON_MIRAIDON || RANDOMIZE_GENERIC_OVERWORLD_POKEMON) | |
| { | |
| var statics = Path.Combine(DEST_MASTER, "world/data/battle/eventBattlePokemon/eventBattlePokemon_array.bin"); | |
| var tableS = FlatBufferConverter.DeserializeFrom<EventBattlePokemonArray>(statics).Table; | |
| StandardizeGiftEncounters(obj.Table, tableS); | |
| UpdateAllIguanaScenes(obj.Table); | |
| } | |
| if (GENERATE_SPOILER_LOG_FILE) | |
| WriteGiftChangesToLog(NewSpecies); | |
| if (!Directory.Exists(dest)) | |
| Directory.CreateDirectory(dest); | |
| var data = FlatBufferConverter.SerializeFrom(obj); | |
| File.WriteAllBytes(Path.Combine(dest, file), data); | |
| UpdateAllGiftScenes(obj.Table); | |
| } | |
| private static List<EventAddPokemon> GetOldGiftEncounters() | |
| { | |
| List<string> Blacklist = [ "add_0193", "add_0195" ]; | |
| const string file = "eventAddPokemon_array.bin"; | |
| var path = Path.Combine(PATH_MASTER, "world/data/event/event_add_pokemon/eventAddPokemon/"); | |
| var obj = FlatBufferConverter.DeserializeFrom<EventAddPokemonArray>(Path.Combine(path, file)); | |
| if (!RANDOMIZE_KORAIDON_MIRAIDON) | |
| { | |
| Blacklist.Add(obj.Table[005].Label); | |
| Blacklist.Add(obj.Table[006].Label); | |
| } | |
| return obj.Table.Where(z => !Blacklist.Contains(z.Label)).Select(z => z).ToList(); | |
| } | |
| private static void ApplyStarterPokemonCriteria(PokeDataFull pk, StarterPokemonCriteria criteria) | |
| { | |
| var species = criteria.Species; | |
| var form = criteria.Form; | |
| var gender = criteria.Gender; | |
| var ability = criteria.Ability; | |
| var nature = criteria.Nature; | |
| var ball = criteria.PokéBall; | |
| var gem = criteria.TeraType; | |
| bool shiny = criteria.Shiny; | |
| // set up DevID | |
| var dev = species == 0 ? pk.DevId : (DevID)GetDevID(species); | |
| // sanitize form | |
| if (species == 0 && form != -1) | |
| form = -1; | |
| // set up National Dex ID | |
| if (species == 0) | |
| species = GetNationalDexID((ushort)dev); | |
| // if criteria is invalid, do not attempt to force the starter | |
| var temp = form == -1 ? 0 : form; | |
| if (species > MAX_SPECIES_ID) | |
| return; | |
| if (form >= GetFormCount((ushort)dev)) | |
| return; | |
| if (form < -1) | |
| return; | |
| if (gender is not (-1 or 1 or 2)) | |
| return; | |
| if (ability is not (-1 or 1 or 2 or 3)) | |
| return; | |
| if (GetIsInvalidScenePokemon(species)) | |
| return; | |
| if (!GetIsPresentInGame((ushort)dev, (byte)temp)) | |
| return; | |
| // apply small corrections | |
| if (species == 0849 && form == 0 && !ToxtricityNaturesAmped.Contains((byte)nature) || species == 0849 && form == 1 && !ToxtricityNaturesLowKey.Contains((byte)nature)) | |
| nature = Nature.Random; | |
| if (species is 1007 or 1008) | |
| form = 0; | |
| if (species == 1017 && form >= 4) | |
| form -= 4; | |
| if (GetPokemonGenderGroup((ushort)dev, (sbyte)temp) == SexGroup.MALE && gender == 2 || GetPokemonGenderGroup((ushort)dev, (sbyte)temp) == SexGroup.FEMALE && gender == 1) | |
| gender = -1; | |
| pk.DevId = dev; | |
| pk.FormId = form == -1 ? GetRandomForm(dev) : form; | |
| pk.Sex = gender == -1 ? GetHasGenderDifference((ushort)dev, pk.FormId) ? GetRandomGender(dev, pk.FormId) : SexType.DEFAULT : (SexType)gender; | |
| pk.Tokusei = ability == -1 ? GetRandomAbilitySetter() : (TokuseiType)(ability + 1); | |
| pk.Seikaku = pk.SeikakuHosei = nature is Nature.Random or Nature.Random25 ? SeikakuType.DEFAULT : (SeikakuType)(nature + 1); | |
| pk.BallId = ball == BallIndex.Default ? pk.BallId : (BallType)ball; | |
| pk.GemType = species is 1017 or 1024 ? GetTeraType(pk.DevId, pk.FormId) : gem == TypeIndex.Default ? pk.GemType : (GemType)(gem + 2); | |
| pk.RareType = shiny ? RareType.RARE : RareType.DEFAULT; | |
| } | |
| private static void RandomizeTrainerPokemon() | |
| { | |
| "Randomizing trainer Pokémon...".Dump(); | |
| const string file = "trdata_array.bin"; | |
| var dest = Path.Combine(DEST_MASTER, "world/data/trainer/trdata/"); | |
| var path = Path.Combine(PATH_MASTER, "world/data/trainer/trdata/"); | |
| var obj = FlatBufferConverter.DeserializeFrom<TrDataMainArray>(Path.Combine(path, file)); | |
| bool IsArvenTitanAlly(string id) => id.StartsWith("pepper_nusi_"); | |
| bool IsDisallowedIncreaseCount(string id) => IsArvenTitanAlly(id) || IsTeraRaidBattleAlly(id) || id is "brother_02_02" or "pepper_00" or "professor_A_02" or "professor_B_02" or "rival_01_hono" or "rival_01_kusa" or "rival_01_mizu" or "sister_02_02"; | |
| bool IsMultiBattleAlly(string id) => id.Contains("_multi"); | |
| bool IsProfessor(string id) => id.StartsWith("professor_"); | |
| bool IsTeamStarBossWithStarmobile(string id) => id.EndsWith("_boss_01"); | |
| bool IsTeraRaidBattleAlly(string id) => id.Contains("raid_assist_NPC_"); | |
| bool IsValidDoubleBattle(string id) => !IsDisallowedIncreaseCount(id) && !IsMultiBattleAlly(id) && !IsProfessor(id) && !IsTeamStarBossWithStarmobile(id) && !IsTeraRaidBattleAlly(id); | |
| foreach (var t in obj.Table) | |
| { | |
| var team = new[] { t.Poke1, t.Poke2, t.Poke3, t.Poke4, t.Poke5, t.Poke6 }; | |
| var partyCount = team.Count(z => z.DevId != DevID.DEV_NULL); | |
| List<DevID> Used = []; | |
| // add Pokémon to party to meet required minimum | |
| if (partyCount < TRAINER_MINIMUM_POKEMON_COUNT && !IsDisallowedIncreaseCount(t.TrId)) | |
| { | |
| var min = Math.Clamp((int)TRAINER_MINIMUM_POKEMON_COUNT, 1, 6); | |
| // Starmobiles are listed as a party Pokémon in the HUD, so only give them 5 | |
| if (min == 6 && IsTeamStarBossWithStarmobile(t.TrId)) | |
| min = 5; | |
| for (int i = 0; i < min; i++) | |
| team[i] = GetTemplateTrainerPokemon(team[0].Level, team[0].BallId, team[0].TalentValue, team[0].EffortValue); | |
| } | |
| // remove Pokémon from party to meet required maximum | |
| if (partyCount > TRAINER_MAXIMUM_POKEMON_COUNT) | |
| { | |
| var max = Math.Clamp((int)TRAINER_MAXIMUM_POKEMON_COUNT, 1, 6); | |
| for (int i = max; i < 6; i++) | |
| team[i] = GetEmptyTrainerPokemon(); | |
| } | |
| // add specific number of Pokémon to party | |
| if ((ADD_NUMBER_OF_POKEMON_TO_REGULAR_TRAINERS != 0 || ADD_NUMBER_OF_POKEMON_TO_IMPORTANT_TRAINERS != 0) && partyCount != 6 && !IsDisallowedIncreaseCount(t.TrId)) | |
| { | |
| bool eligible = (ADD_NUMBER_OF_POKEMON_TO_REGULAR_TRAINERS != 0 && !IsImportantTrainer(t.TrId)) || (ADD_NUMBER_OF_POKEMON_TO_IMPORTANT_TRAINERS != 0 && IsImportantTrainer(t.TrId)); | |
| if (eligible) | |
| { | |
| var firstEmptySlot = team.FirstOrDefault(z => z.DevId == DevID.DEV_NULL); | |
| var last = team.IndexOf(firstEmptySlot); | |
| var add = IsImportantTrainer(t.TrId) ? Math.Clamp((int)ADD_NUMBER_OF_POKEMON_TO_IMPORTANT_TRAINERS, 1, 5) : Math.Clamp((int)ADD_NUMBER_OF_POKEMON_TO_REGULAR_TRAINERS, 1, 5); | |
| for (int i = 0; i < add; i++) | |
| { | |
| // they now have a full party | |
| if (team[5].DevId != DevID.DEV_NULL) | |
| continue; | |
| // Starmobiles are listed as a party Pokémon in the HUD, so only give them 5 | |
| if (IsTeamStarBossWithStarmobile(t.TrId) && last == 5) | |
| continue; | |
| team[last] = GetTemplateTrainerPokemon(team[0].Level, team[0].BallId, team[0].TalentValue, team[0].EffortValue); | |
| last++; | |
| } | |
| } | |
| } | |
| // turn all battles into double battles, with some exceptions | |
| if (MAKE_ALL_TRAINER_BATTLES_DOUBLE_BATTLES && IsValidDoubleBattle(t.TrId)) | |
| { | |
| if ((partyCount & 1) != 0) | |
| team[partyCount] = GetTemplateTrainerPokemon(team[0].Level, team[0].BallId, team[0].TalentValue, team[0].EffortValue); | |
| t.BattleType = BattleType.DOUBLE; | |
| t.AiDouble = true; | |
| } | |
| if (GIVE_IMPORTANT_TRAINERS_SIX_POKEMON && IsImportantTrainer(t.TrId)) | |
| { | |
| // fill empty slots with a placeholder | |
| var max = IsTeamStarBossWithStarmobile(t.TrId) ? 5 : 6; | |
| for (int i = 0; i < max; i++) | |
| { | |
| if (team[i].DevId != DevID.DEV_NULL) | |
| continue; | |
| team[i] = GetTemplateTrainerPokemon(team[0].Level, team[0].BallId, team[0].TalentValue, team[0].EffortValue); | |
| } | |
| } | |
| if (ALLOW_ALL_TRAINERS_TO_TERASTALLIZE && !IsArvenTitanAlly(t.TrId)) | |
| t.ChangeGem = true; | |
| // refresh team data before randomizing, and sort by level ascending, with empty slots at the end | |
| team = team.OrderBy(z => z.Level == 0).ThenBy(z => z.Level).ToArray(); | |
| partyCount = team.Count(z => z.DevId != DevID.DEV_NULL); | |
| t.Poke1 = team[0]; | |
| t.Poke2 = team[1]; | |
| t.Poke3 = team[2]; | |
| t.Poke4 = team[3]; | |
| t.Poke5 = team[4]; | |
| t.Poke6 = team[5]; | |
| foreach (var pk in team.Where(z => z.DevId != DevID.DEV_NULL)) | |
| { | |
| if (!RANDOMIZE_KORAIDON_MIRAIDON && t.TrId is "professor_A_02" or "professor_B_02") | |
| continue; | |
| if (TYPE_THEMED_IMPORTANT_TRAINERS != TypeThemeRandomizer.NotTypeThemed && TypeThemedTrainers.Any(z => z.Contains(t.TrId))) | |
| continue; | |
| var firstEmptySlot = team.FirstOrDefault(z => z.DevId == DevID.DEV_NULL); | |
| var last = firstEmptySlot == null ? 5 : team.IndexOf(firstEmptySlot) - 1; | |
| bool tera = t.ChangeGem && team.IndexOf(pk) == last; | |
| RandomizeTrainerPokemonSingle(pk, tera, false, IsArvenTitanAlly(t.TrId), IsImportantTrainer(t.TrId), partyCount, t.BattleType); | |
| // ensure all teams consist of unique species, only if we have a large enough pool | |
| if (SpeciesList.Count >= 200) | |
| { | |
| while (Used.Contains(pk.DevId)) | |
| RandomizeTrainerPokemonSingle(pk, tera, true, IsArvenTitanAlly(t.TrId), IsImportantTrainer(t.TrId), partyCount, t.BattleType); | |
| } | |
| Used.Add(pk.DevId); | |
| } | |
| // if AI Sada/Turo's final Pokémon is a Paradox Pokémon and it has Protosynthesis or Quark Drive, give it a Booster Energy, true to the vanilla game | |
| if (FORCE_SWAP_SPECIES_PARADOX && ALLOW_SPECIES_GEN_9 && ALLOW_SPECIES_PARADOX && !RANDOMIZE_POKEMON_ABILITIES && TRAINER_HELD_ITEM_DISTRIBUTION != TrainerHeldItemRandomizer.NoHeldItems && t.TrId is "professor_A_01" or "professor_B_01") | |
| { | |
| var firstEmptySlot = team.FirstOrDefault(z => z.DevId == DevID.DEV_NULL); | |
| var last = firstEmptySlot == null ? 5 : team.IndexOf(firstEmptySlot) - 1; | |
| team[last].Item = ItemID.ITEMID_BUUSUTOENAJII; | |
| } | |
| if (GIVE_TRAINERS_MAXIMUM_AI) | |
| { | |
| t.AiBasic = true; | |
| t.AiHigh = true; | |
| t.AiExpert = true; | |
| t.AiWeak = false; | |
| t.AiChange = true; | |
| } | |
| } | |
| if (RIVAL_CARRIES_STARTER_THROUGHOUT_GAME) | |
| ForceStarterSelectionsOnRivals(obj.Table); | |
| if (TYPE_THEMED_IMPORTANT_TRAINERS != TypeThemeRandomizer.NotTypeThemed) | |
| RandomizeTypeThemedTrainers(obj.Table); | |
| var statics = Path.Combine(DEST_MASTER, "world/data/battle/eventBattlePokemon/eventBattlePokemon_array.bin"); | |
| var tableS = FlatBufferConverter.DeserializeFrom<EventBattlePokemonArray>(statics).Table; | |
| StandardizeTrainerData(obj.Table, tableS); | |
| if (GENERATE_SPOILER_LOG_FILE) | |
| WriteTrainerChangesToLog(obj.Table); | |
| if (!Directory.Exists(dest)) | |
| Directory.CreateDirectory(dest); | |
| var data = FlatBufferConverter.SerializeFrom(obj); | |
| File.WriteAllBytes(Path.Combine(dest, file), data); | |
| UpdateAllTrainerScenes(obj.Table); | |
| } | |
| private static PokeDataBattle GetEmptyTrainerPokemon() => new PokeDataBattle() | |
| { | |
| DevId = DevID.DEV_NULL, | |
| Level = 0, | |
| BallId = BallType.NONE, | |
| Waza1 = GetCleanMoveSlot(), | |
| Waza2 = GetCleanMoveSlot(), | |
| Waza3 = GetCleanMoveSlot(), | |
| Waza4 = GetCleanMoveSlot(), | |
| TalentValue = new ParamSet(), | |
| EffortValue = new ParamSet(), | |
| ScaleType = SizeType.VALUE, | |
| ScaleValue = 128, | |
| }; | |
| private static PokeDataBattle GetTemplateTrainerPokemon(int level, BallType ball, ParamSet talent, ParamSet effort) => new PokeDataBattle() | |
| { | |
| DevId = DevID.DEV_HUSIGIDANE, | |
| Level = level, | |
| BallId = ball, | |
| Waza1 = GetCleanMoveSlot(), | |
| Waza2 = GetCleanMoveSlot(), | |
| Waza3 = GetCleanMoveSlot(), | |
| Waza4 = GetCleanMoveSlot(), | |
| TalentValue = talent, | |
| EffortValue = effort, | |
| ScaleType = SizeType.VALUE, | |
| ScaleValue = 128, | |
| }; | |
| private static void RandomizeTrainerPokemonSingle(PokeDataBattle pk, bool tera, bool repeat, bool pepper, bool important, int partyCount, BattleType battleType) | |
| { | |
| if (RANDOMIZE_POKEMON_TO_HAVE_SIMILAR_STRENGTH) | |
| { | |
| var ss = GetSimilarStrengthPokemon(pk.DevId, pk.FormId); | |
| pk.DevId = ss.Species; | |
| pk.FormId = pepper ? (short)0 : ss.Form; | |
| } | |
| else | |
| { | |
| pk.DevId = GetRandomSpecies(pk.DevId); | |
| pk.FormId = pepper ? (short)0 : GetRandomForm(pk.DevId); | |
| } | |
| if (GIVE_OPPONENT_SMEARGLE_RANDOM_MOVESETS && pk.DevId == DevID.DEV_DOOBURU) | |
| { | |
| var moves = GetRandomSmeargleMoveset(); | |
| pk.WazaType = WazaType.MANUAL; | |
| pk.Waza1.WazaId = moves[0]; | |
| pk.Waza2.WazaId = moves[1]; | |
| pk.Waza3.WazaId = moves[2]; | |
| pk.Waza4.WazaId = moves[3]; | |
| } | |
| else | |
| { | |
| pk.WazaType = WazaType.DEFAULT; | |
| pk.Waza1 = GetCleanMoveSlot(); | |
| pk.Waza2 = GetCleanMoveSlot(); | |
| pk.Waza3 = GetCleanMoveSlot(); | |
| pk.Waza4 = GetCleanMoveSlot(); | |
| } | |
| pk.Sex = GetRandomGender(pk.DevId, pk.FormId); | |
| pk.GemType = tera ? GetTeraType(pk.DevId, pk.FormId, true) : GemType.DEFAULT; | |
| pk.Seikaku = GetRandomNature(pk.DevId, pk.FormId); | |
| pk.Tokusei = GetRandomAbilitySlot(); | |
| pk.RareType = pepper ? RareType.NO_RARE : GetRandom(100) < TRAINER_POKEMON_SHINY_CHANCE_PERCENT ? RareType.RARE : RareType.NO_RARE; | |
| if (GIVE_POKEMON_RANDOM_POKE_BALLS) | |
| pk.BallId = GetRandomBall(); | |
| if (MODIFY_POKEMON_LEVELS && !repeat) | |
| pk.Level = GetModifiedLevel(pk.Level); | |
| if (GIVE_TRAINERS_MAX_INDIVIDUAL_VALUES || GIVE_ALL_POKEMON_MAX_INDIVIDUAL_VALUES) | |
| { | |
| pk.TalentType = TalentType.VALUE; | |
| pk.TalentValue = FlawlessIVSpread; | |
| } | |
| if (FORCE_FULLY_EVOLVED_POKEMON && pk.Level >= FORCE_FULLY_EVOLVED_POKEMON_LEVEL) | |
| { | |
| var ss = ForceEvolveSpeciesFully(pk.DevId, pk.FormId); | |
| pk.DevId = ss.Species; | |
| pk.FormId = pepper ? (short)0 : ss.Form; | |
| pk.Sex = GetRandomGender(pk.DevId, pk.FormId); // refresh | |
| // refresh Toxtricity nature | |
| if (pk.DevId == DevID.DEV_SUTORINDAA) | |
| pk.Seikaku = GetRandomNature(pk.DevId, pk.FormId); | |
| } | |
| if (GIVE_TRAINERS_SMART_EFFORT_VALUES) | |
| pk.EffortValue = SetSmartEVs(pk.DevId, pk.FormId, pk.Level); | |
| if (REMOVE_EFFORT_VALUE_YIELDS) | |
| pk.EffortValue = EmptyEVSpread; | |
| if (FORCE_TERASTALLIZED_POKEMON_TO_HAVE_ONE_STAB_TERA_MOVE && tera && pk.GemType != GemType.NIJI) | |
| ApplyRevisedTerastallizedMoveset(pk); | |
| pk.Item = TRAINER_HELD_ITEM_DISTRIBUTION switch | |
| { | |
| _ when IsFormHeldItemDependent(pk.DevId, pk.FormId) => GetTrainerPokemonHeldItem(pk, tera, partyCount, battleType), | |
| TrainerHeldItemRandomizer.NoHeldItems => ItemID.ITEMID_NONE, | |
| TrainerHeldItemRandomizer.UnchangedHeldItems => pk.Item, | |
| TrainerHeldItemRandomizer.ImportantTrainersOnly when !important => ItemID.ITEMID_NONE, | |
| _ => GetTrainerPokemonHeldItem(pk, tera, partyCount, battleType), | |
| }; | |
| } | |
| private static void RandomizeTrainerPokemonTypeThemed(PokeDataBattle pk, byte type, bool tera, bool repeat, bool important, int partyCount, BattleType battleType) | |
| { | |
| // Tera Type for ace will match themed type, but the original types will not, just like base game | |
| if (tera) | |
| { | |
| pk.DevId = GetRandomSpecies(pk.DevId); | |
| pk.FormId = GetRandomForm(pk.DevId); | |
| // since these species need to be specific Tera Types, don't even consider them | |
| while (pk.DevId is DevID.DEV_KAMENONI or DevID.DEV_KODAIGAME) | |
| { | |
| pk.DevId = GetRandomSpecies(pk.DevId); | |
| pk.FormId = GetRandomForm(pk.DevId); | |
| } | |
| // ensure the original types do not match the themed type, only if we have a large enough pool | |
| if (SpeciesList.Count >= 200) | |
| { | |
| var types = GetPokemonTypes(pk.DevId, pk.FormId); | |
| while (pk.DevId is DevID.DEV_KAMENONI or DevID.DEV_KODAIGAME || types.Type1 == type || types.Type2 == type) | |
| { | |
| pk.DevId = GetRandomSpecies(pk.DevId); | |
| pk.FormId = GetRandomForm(pk.DevId); | |
| types = GetPokemonTypes(pk.DevId, pk.FormId); // refresh | |
| } | |
| } | |
| pk.GemType = (GemType)(type + 2); | |
| } | |
| else | |
| { | |
| var tt = GetTypeThemedPokemon(type); | |
| pk.DevId = tt.Species; | |
| pk.FormId = tt.Form; | |
| pk.GemType = GemType.DEFAULT; | |
| } | |
| if (GIVE_OPPONENT_SMEARGLE_RANDOM_MOVESETS && pk.DevId == DevID.DEV_DOOBURU) | |
| { | |
| var moves = GetRandomSmeargleMoveset(); | |
| pk.WazaType = WazaType.MANUAL; | |
| pk.Waza1.WazaId = moves[0]; | |
| pk.Waza2.WazaId = moves[1]; | |
| pk.Waza3.WazaId = moves[2]; | |
| pk.Waza4.WazaId = moves[3]; | |
| } | |
| else | |
| { | |
| pk.WazaType = WazaType.DEFAULT; | |
| pk.Waza1 = GetCleanMoveSlot(); | |
| pk.Waza2 = GetCleanMoveSlot(); | |
| pk.Waza3 = GetCleanMoveSlot(); | |
| pk.Waza4 = GetCleanMoveSlot(); | |
| } | |
| pk.Sex = GetRandomGender(pk.DevId, pk.FormId); | |
| pk.Seikaku = GetRandomNature(pk.DevId, pk.FormId); | |
| pk.Tokusei = GetRandomAbilitySlot(); | |
| pk.RareType = GetRandom(100) < TRAINER_POKEMON_SHINY_CHANCE_PERCENT ? RareType.RARE : RareType.NO_RARE; | |
| if (GIVE_POKEMON_RANDOM_POKE_BALLS) | |
| pk.BallId = GetRandomBall(); | |
| if (MODIFY_POKEMON_LEVELS && !repeat) | |
| pk.Level = GetModifiedLevel(pk.Level); | |
| if (GIVE_TRAINERS_MAX_INDIVIDUAL_VALUES || GIVE_ALL_POKEMON_MAX_INDIVIDUAL_VALUES) | |
| { | |
| pk.TalentType = TalentType.VALUE; | |
| pk.TalentValue = FlawlessIVSpread; | |
| } | |
| if (FORCE_FULLY_EVOLVED_POKEMON && pk.Level >= FORCE_FULLY_EVOLVED_POKEMON_LEVEL) | |
| { | |
| var ss = ForceEvolveSpeciesFully(pk.DevId, pk.FormId); | |
| pk.DevId = ss.Species; | |
| pk.FormId = ss.Form; | |
| pk.Sex = GetRandomGender(pk.DevId, pk.FormId); // refresh | |
| // refresh Toxtricity nature | |
| if (pk.DevId == DevID.DEV_SUTORINDAA) | |
| pk.Seikaku = GetRandomNature(pk.DevId, pk.FormId); | |
| } | |
| if (GIVE_TRAINERS_SMART_EFFORT_VALUES) | |
| pk.EffortValue = SetSmartEVs(pk.DevId, pk.FormId, pk.Level); | |
| if (REMOVE_EFFORT_VALUE_YIELDS) | |
| pk.EffortValue = EmptyEVSpread; | |
| if (FORCE_TERASTALLIZED_POKEMON_TO_HAVE_ONE_STAB_TERA_MOVE && tera && pk.GemType != GemType.NIJI) | |
| ApplyRevisedTerastallizedMoveset(pk); | |
| pk.Item = TRAINER_HELD_ITEM_DISTRIBUTION switch | |
| { | |
| _ when IsFormHeldItemDependent(pk.DevId, pk.FormId) => GetTrainerPokemonHeldItem(pk, tera, partyCount, battleType), | |
| TrainerHeldItemRandomizer.NoHeldItems => ItemID.ITEMID_NONE, | |
| TrainerHeldItemRandomizer.UnchangedHeldItems => pk.Item, | |
| TrainerHeldItemRandomizer.ImportantTrainersOnly when !important => ItemID.ITEMID_NONE, | |
| _ => GetTrainerPokemonHeldItem(pk, tera, partyCount, battleType), | |
| }; | |
| } | |
| private static void ForceStarterSelectionsOnRivals(IList<TrDataMain> table) | |
| { | |
| var gifts = Path.Combine(DEST_MASTER, "world/data/event/event_add_pokemon/eventAddPokemon/eventAddPokemon_array.bin"); | |
| var tableG = FlatBufferConverter.DeserializeFrom<EventAddPokemonArray>(gifts).Table; | |
| List<SexType> Genders = []; | |
| List<GemType> TeraTypes = []; | |
| List<SeikakuType> Natures = []; | |
| List<TokuseiType> Abilities = []; | |
| // convert from random to a fixed selection | |
| for (int i = 0; i <= 2; i++) | |
| { | |
| var pk = tableG[i].PokeData; | |
| Genders.Add(GetRandomGender(pk.DevId, pk.FormId)); | |
| TeraTypes.Add(GetTeraType(pk.DevId, pk.FormId, true)); | |
| Natures.Add(GetRandomNature(pk.DevId, pk.FormId)); | |
| Abilities.Add(GetRandomAbilitySlot()); | |
| } | |
| // initialize | |
| var G = tableG[001].PokeData; | |
| var F = tableG[000].PokeData; | |
| var W = tableG[002].PokeData; | |
| var G1 = new PokeDataFull { DevId = G.DevId, FormId = G.FormId, Sex = Genders[1], Item = G.Item, BallId = G.BallId, GemType = TeraTypes[1], Seikaku = Natures[1], Tokusei = Abilities[1], RareType = G.RareType }; | |
| var F1 = new PokeDataFull { DevId = F.DevId, FormId = F.FormId, Sex = Genders[0], Item = F.Item, BallId = F.BallId, GemType = TeraTypes[0], Seikaku = Natures[0], Tokusei = Abilities[0], RareType = F.RareType }; | |
| var W1 = new PokeDataFull { DevId = W.DevId, FormId = W.FormId, Sex = Genders[2], Item = W.Item, BallId = W.BallId, GemType = TeraTypes[2], Seikaku = Natures[2], Tokusei = Abilities[2], RareType = W.RareType }; | |
| var G2 = ForceEvolveSpeciesOnce(new PokeDataFull { DevId = G1.DevId, FormId = G1.FormId, Sex = G1.Sex, Item = G1.Item, BallId = G1.BallId, GemType = G1.GemType, Seikaku = G1.Seikaku, Tokusei = G1.Tokusei, RareType = G1.RareType }); | |
| var F2 = ForceEvolveSpeciesOnce(new PokeDataFull { DevId = F1.DevId, FormId = F1.FormId, Sex = F1.Sex, Item = F1.Item, BallId = F1.BallId, GemType = F1.GemType, Seikaku = F1.Seikaku, Tokusei = F1.Tokusei, RareType = F1.RareType }); | |
| var W2 = ForceEvolveSpeciesOnce(new PokeDataFull { DevId = W1.DevId, FormId = W1.FormId, Sex = W1.Sex, Item = W1.Item, BallId = W1.BallId, GemType = W1.GemType, Seikaku = W1.Seikaku, Tokusei = W1.Tokusei, RareType = W1.RareType }); | |
| var G3 = ForceEvolveSpeciesOnce(new PokeDataFull { DevId = G2.DevId, FormId = G2.FormId, Sex = G2.Sex, Item = G2.Item, BallId = G2.BallId, GemType = G2.GemType, Seikaku = G2.Seikaku, Tokusei = G2.Tokusei, RareType = G2.RareType }); | |
| var F3 = ForceEvolveSpeciesOnce(new PokeDataFull { DevId = F2.DevId, FormId = F2.FormId, Sex = F2.Sex, Item = F2.Item, BallId = F2.BallId, GemType = F2.GemType, Seikaku = F2.Seikaku, Tokusei = F2.Tokusei, RareType = F2.RareType }); | |
| var W3 = ForceEvolveSpeciesOnce(new PokeDataFull { DevId = W2.DevId, FormId = W2.FormId, Sex = W2.Sex, Item = W2.Item, BallId = W2.BallId, GemType = W2.GemType, Seikaku = W2.Seikaku, Tokusei = W2.Tokusei, RareType = W2.RareType }); | |
| bool IsNemona(string id) => id.Contains("rival_") || id.Contains("nemo_"); | |
| bool IsNemonaStage1(string id) => IsNemona(id) && !id.Contains("rival_02_01") && (id.Contains("rival_01_") || id.Contains("rival_02_") || id.Contains("rival_03_")); | |
| bool IsNemonaStage2(string id) => IsNemona(id) && id.Contains("rival_04_"); | |
| bool IsClavell(string id) => id.Contains("clavel_") || id.Contains("claver_"); | |
| foreach (var t in table.Where(z => IsNemona(z.TrId) || IsClavell(z.TrId))) | |
| { | |
| // unused rival battles | |
| if (!t.TrId.Contains("kusa") && !t.TrId.Contains("hono") && !t.TrId.Contains("mizu")) | |
| continue; | |
| var team = new[] { t.Poke1, t.Poke2, t.Poke3, t.Poke4, t.Poke5, t.Poke6 }; | |
| bool tutorial = !t.TrId.Contains("rival_02_01") && t.TrId.StartsWith("rival_02_"); | |
| bool tera = t.ChangeGem && !tutorial && !t.TrId.StartsWith("rival_01_"); | |
| var partyCount = team.Count(z => z.DevId != DevID.DEV_NULL); | |
| var ace = team.IndexOf(team.OrderBy(z => z.Level).Last()); | |
| var index = tutorial ? 0 : ace; // starter is always last slot, except for Nemona's Terastal tutorial battle | |
| var pk = team[index]; | |
| var starter = t.TrId switch | |
| { | |
| // Clavell | |
| _ when IsClavell(t.TrId) && t.TrId.Contains("kusa") => F3, | |
| _ when IsClavell(t.TrId) && t.TrId.Contains("hono") => W3, | |
| _ when IsClavell(t.TrId) && t.TrId.Contains("mizu") => G3, | |
| // Nemona (initial) | |
| _ when IsNemonaStage1(t.TrId) && t.TrId.Contains("kusa") => W1, | |
| _ when IsNemonaStage1(t.TrId) && t.TrId.Contains("hono") => G1, | |
| _ when IsNemonaStage1(t.TrId) && t.TrId.Contains("mizu") => F1, | |
| // Nemona (first evolution) | |
| _ when IsNemonaStage2(t.TrId) && t.TrId.Contains("kusa") => W2, | |
| _ when IsNemonaStage2(t.TrId) && t.TrId.Contains("hono") => G2, | |
| _ when IsNemonaStage2(t.TrId) && t.TrId.Contains("mizu") => F2, | |
| // Nemona (second evolution) | |
| _ when t.TrId.Contains("kusa") => W3, | |
| _ when t.TrId.Contains("hono") => G3, | |
| _ when t.TrId.Contains("mizu") => F3, | |
| _ => throw new ArgumentException(nameof(t.TrId), t.TrId), | |
| }; | |
| pk.DevId = starter.DevId; | |
| pk.FormId = starter.FormId; | |
| pk.Sex = starter.Sex; | |
| pk.Item = starter.Item; | |
| pk.BallId = starter.BallId; | |
| pk.GemType = starter.GemType; | |
| pk.Seikaku = starter.Seikaku; | |
| pk.Tokusei = starter.Tokusei; | |
| pk.RareType = starter.RareType == RareType.RARE ? RareType.RARE : RareType.NO_RARE; | |
| if (GIVE_TRAINERS_SMART_EFFORT_VALUES) | |
| pk.EffortValue = SetSmartEVs(pk.DevId, pk.FormId, pk.Level); | |
| if (REMOVE_EFFORT_VALUE_YIELDS) | |
| pk.EffortValue = EmptyEVSpread; | |
| if (GIVE_OPPONENT_SMEARGLE_RANDOM_MOVESETS && pk.DevId == DevID.DEV_DOOBURU && t.TrId is not ("rival_01_hono" or "rival_01_kusa" or "rival_01_mizu")) | |
| { | |
| var moves = GetRandomSmeargleMoveset(); | |
| pk.WazaType = WazaType.MANUAL; | |
| pk.Waza1.WazaId = moves[0]; | |
| pk.Waza2.WazaId = moves[1]; | |
| pk.Waza3.WazaId = moves[2]; | |
| pk.Waza4.WazaId = moves[3]; | |
| } | |
| if (FORCE_TERASTALLIZED_POKEMON_TO_HAVE_ONE_STAB_TERA_MOVE && tera && pk.GemType != GemType.NIJI) | |
| ApplyRevisedTerastallizedMoveset(pk); | |
| pk.Item = TRAINER_HELD_ITEM_DISTRIBUTION switch | |
| { | |
| _ when IsFormHeldItemDependent(pk.DevId, pk.FormId) => GetTrainerPokemonHeldItem(pk, ace != 0, partyCount, t.BattleType), | |
| TrainerHeldItemRandomizer.NoHeldItems => ItemID.ITEMID_NONE, | |
| TrainerHeldItemRandomizer.UnchangedHeldItems => pk.Item, | |
| _ => GetTrainerPokemonHeldItem(pk, ace != 0, partyCount, t.BattleType), | |
| }; | |
| } | |
| } | |
| private static void RandomizeTypeThemedTrainers(IList<TrDataMain> table) | |
| { | |
| foreach (var set in TypeThemedTrainers) | |
| { | |
| var idx = TypeThemedTrainers.IndexOf(set); | |
| var type = idx switch | |
| { | |
| >= 0 and <= 12 => ThemedTypes[idx], // Gym Leaders, Team Star | |
| 13 => ThemedTypes[16], // Rika | |
| 14 => ThemedTypes[15], // Poppy | |
| 15 => ThemedTypes[14], // Larry | |
| 16 => ThemedTypes[17], // Hassel | |
| 17 => ThemedTypes[13], // Tyme | |
| 18 => ThemedTypes[12], // Dendra | |
| 19 => ThemedTypes[09], // Crispin | |
| 20 => ThemedTypes[15], // Amarys | |
| 21 => ThemedTypes[11], // Lacey | |
| 22 => ThemedTypes[17], // Drayton | |
| _ => throw new ArgumentException(nameof(idx)), | |
| }; | |
| foreach (var t in set) | |
| { | |
| var tr = table.FirstOrDefault(z => z.TrId == t); | |
| var team = new[] { tr.Poke1, tr.Poke2, tr.Poke3, tr.Poke4, tr.Poke5, tr.Poke6 }; | |
| var firstEmptySlot = team.FirstOrDefault(z => z.DevId == DevID.DEV_NULL); | |
| var last = firstEmptySlot == null ? 5 : team.IndexOf(firstEmptySlot) - 1; | |
| var partyCount = team.Count(z => z.DevId != DevID.DEV_NULL); | |
| List<DevID> Used = []; | |
| foreach (var pk in team.Where(z => z.DevId != DevID.DEV_NULL)) | |
| { | |
| // Larry uses both of his type specialties when battled at Blueberry Academy | |
| if (tr.TrId == "su2_bukatu_hikou" && team.IndexOf(pk) >= partyCount / 2) | |
| type = ThemedTypes[14]; | |
| bool tera = tr.ChangeGem && team.IndexOf(pk) == last; | |
| RandomizeTrainerPokemonTypeThemed(pk, type, tera, false, true, partyCount, tr.BattleType); | |
| // ensure all teams consist of unique species, only if we have a large enough pool | |
| if (SpeciesList.Count >= 200) | |
| { | |
| while (Used.Contains(pk.DevId)) | |
| RandomizeTrainerPokemonTypeThemed(pk, type, tera, true, true, partyCount, tr.BattleType); | |
| } | |
| // ensure Pokémon that are forced to evolve still abide by the themed type | |
| var types = GetPokemonTypes(pk.DevId, pk.FormId); | |
| while (!tera && types.Type1 != type && types.Type2 != type) | |
| { | |
| RandomizeTrainerPokemonTypeThemed(pk, type, tera, true, true, partyCount, tr.BattleType); | |
| types = GetPokemonTypes(pk.DevId, pk.FormId); // refresh | |
| } | |
| Used.Add(pk.DevId); | |
| } | |
| } | |
| } | |
| } | |
| private static ParamSet SetSmartEVs(DevID species, short form, int level) | |
| { | |
| var baseStats = GetBaseStats(species, form); | |
| var index1 = GetHighestStatIndex(baseStats); baseStats[index1] = 0; // highest stat | |
| var index2 = GetHighestStatIndex(baseStats); baseStats[index2] = 0; // second-highest stat | |
| var index3 = GetHighestStatIndex(baseStats); baseStats[index3] = 0; // third-highest stat | |
| var statLow = level > 45 ? 6 : 0; | |
| var statHigh = level switch | |
| { | |
| >= 1 and <= 25 => 80, | |
| >= 26 and <= 35 => 140, | |
| >= 36 and <= 45 => 200, | |
| _ => 252, | |
| }; | |
| var ev = new[] { 0, 0, 0, 0, 0, 0 }; | |
| ev[index1] = (byte)statHigh; | |
| ev[index2] = (byte)statHigh; | |
| ev[index3] = (byte)statLow; | |
| var spread = new ParamSet() { HP = ev[0], ATK = ev[1], DEF = ev[2], SPA = ev[3], SPD = ev[4], SPE = ev[5] }; | |
| return spread; | |
| } | |
| private static int GetHighestStatIndex(byte[] stats) | |
| { | |
| var highest = stats.Max(); | |
| bool hasDuplicateStat = stats.Where(z => z == highest).GroupBy(z => z).Any(z => z.Count() > 1); | |
| // if we have more than 1 stat with the same value, randomly select one of them, instead of defaulting to the first instance of it | |
| if (hasDuplicateStat) | |
| { | |
| List<int> DuplicateIndices = []; | |
| for (int i = 0; i < 6; i++) | |
| { | |
| if (stats[i] == highest) | |
| DuplicateIndices.Add(i); | |
| } | |
| var rand = DuplicateIndices[GetRandom(DuplicateIndices.Count)]; | |
| return rand; | |
| } | |
| return stats.IndexOf(highest); | |
| } | |
| private static void ApplyRevisedTerastallizedMoveset(PokeDataBattle pk) | |
| { | |
| var species = GetNationalDexID((ushort)pk.DevId); | |
| var form = pk.FormId; | |
| var pi = PersonalInfoEdited.Table.FirstOrDefault(z => z.Info.SpeciesNational == species && z.Info.Form == form); | |
| var primaryType = pi.Type1; | |
| var gem = pk.GemType; | |
| var typeExpected = (byte)(gem - 2); | |
| var level = pk.Level; | |
| var ability = GetCurrentAbility(pk.DevId, form, pk.Tokusei); | |
| var moves = GIVE_OPPONENT_SMEARGLE_RANDOM_MOVESETS && pk.DevId == DevID.DEV_DOOBURU ? [(ushort)pk.Waza1.WazaId, (ushort)pk.Waza2.WazaId, (ushort)pk.Waza3.WazaId, (ushort)pk.Waza4.WazaId] | |
| : GetCurrentMoves(pk.DevId, pk.FormId, pk.Level); | |
| if (!moves.Any(z => GetMoveTypeAfterModifiers(species, (ushort)form, z, ability, primaryType, gem, true) == typeExpected && !GetMoveIsUnaffectedBySTAB(z) && !GetMoveIsStatus(z))) | |
| { | |
| var firstEmptySlot = !moves.Contains(0) ? 3 : moves.IndexOf(0); | |
| var teraMoves = GetMovesMatchingTeraType(species, form, moves, ability, primaryType, gem, level); | |
| if (teraMoves.Count != 0) | |
| { | |
| var selectedMove = teraMoves[GetRandom(teraMoves.Count)]; | |
| moves[firstEmptySlot] = (ushort)selectedMove; | |
| pk.WazaType = WazaType.MANUAL; | |
| pk.Waza1.WazaId = (WazaID)moves[0]; | |
| pk.Waza2.WazaId = (WazaID)moves[1]; | |
| pk.Waza3.WazaId = (WazaID)moves[2]; | |
| pk.Waza4.WazaId = (WazaID)moves[3]; | |
| } | |
| } | |
| } | |
| private static List<WazaID> GetMovesMatchingTeraType(ushort species, short form, List<ushort> moves, ushort ability, byte primaryType, GemType gem, int level) | |
| { | |
| var typeExpected = (byte)(gem - 2); | |
| if (GIVE_OPPONENT_SMEARGLE_RANDOM_MOVESETS && species == 0235) | |
| { | |
| var move = GetRandomMoveList().FirstOrDefault(z => !GetMoveIsStatus((ushort)z) | |
| && !GetMoveIsUnaffectedBySTAB((ushort)z) | |
| && !moves.Contains((ushort)z) | |
| && GetMoveCanSketch((ushort)z) | |
| && GetMoveTypeAfterModifiers(species, (ushort)form, (ushort)z, ability, primaryType, gem, true) == typeExpected); | |
| return [(WazaID)move]; | |
| } | |
| var vanilla = PersonalInfoVanilla.Table.FirstOrDefault(z => z.Info.SpeciesNational == species && z.Info.Form == form); | |
| var pk = PersonalInfoEdited.Table.FirstOrDefault(z => z.Info.SpeciesNational == species && z.Info.Form == form); | |
| var movesTMV = vanilla.TechnicalMachine.Where(z => PersonalDumperSV.TMIndexes[1..].Contains(z)); | |
| var movesTMR = pk.TechnicalMachine; | |
| var movesL = pk.Learnset.Where(z => z.Level <= level).Select(z => z.Move); | |
| var movesR = pk.ReminderMoves; | |
| var movesE = pk.EggMoves; | |
| var movesAll = movesL.Concat(movesR).Concat(movesE).ToList(); | |
| // permit TMs | |
| if (TERASTAL_EXTRA_MOVE_SOURCE == TerastalMoveRandomizer.AddVanillaTMs) movesAll = movesAll.Concat(movesTMV).ToList(); | |
| if (TERASTAL_EXTRA_MOVE_SOURCE == TerastalMoveRandomizer.AddRandomizedTMs) movesAll = movesAll.Concat(movesTMR).ToList(); | |
| // add Tera Blast regardless | |
| if (vanilla.TechnicalMachine.Contains(851)) | |
| movesAll.Add(851); | |
| // potential form mismatch | |
| if (species == 0647 && movesAll.Contains(548)) | |
| movesAll.Remove(548); | |
| var result = movesAll.Where(z => GetMoveTypeAfterModifiers(species, (ushort)form, z, ability, primaryType, gem, true) == typeExpected && !GetMoveIsUnaffectedBySTAB(z) && !GetMoveIsStatus(z)).Select(z => (WazaID)z).Distinct().ToList(); | |
| return result; | |
| } | |
| private static ItemID GetTrainerPokemonHeldItem(PokeDataBattle pk, bool tera, int partyCount, BattleType battleType) | |
| { | |
| // do not give any other held item | |
| if (IsFormHeldItemDependent(pk.DevId, pk.FormId) && pk.FormId != 0) | |
| return GetFormSpecificItem(pk.DevId, pk.FormId); | |
| // if it's too early in the game for the player to have good held items, the NPCs shouldn't have access to them, either | |
| if (pk.Level < 10) | |
| return ItemID.ITEMID_NONE; | |
| var ability = GetCurrentAbility(pk.DevId, pk.FormId, pk.Tokusei); | |
| // Klutz: items will have no effect | |
| if (ability == 103) | |
| return ItemID.ITEMID_NONE; | |
| // Imposter: can't be sure of the Pokémon it transforms into | |
| if (ability == 150) | |
| return (ItemID)GeneralPurposeHeldItems[GetRandom(GeneralPurposeHeldItems.Count)]; | |
| List<ushort> PotentialHeldItems = []; | |
| bool IsMoveRampaging(ushort move) => GetMoveEffectSequence(move) == 27; | |
| bool IsMoveOneHitKO(ushort move) => GetMoveEffectSequence(move) == 38; | |
| var pi = PersonalInfoEdited.Table.FirstOrDefault(z => z.Info.SpeciesInternal == (ushort)pk.DevId && z.Info.Form == pk.FormId); | |
| var gender = pi.Gender.Group; | |
| var baseStats = GetBaseStats(pk.DevId, pk.FormId); | |
| var iv = pk.TalentValue; | |
| var ev = pk.EffortValue; | |
| var level = pk.Level; | |
| var nature = pk.Seikaku; | |
| var moves = pk.WazaType == WazaType.MANUAL ? [(ushort)pk.Waza1.WazaId, (ushort)pk.Waza2.WazaId, (ushort)pk.Waza3.WazaId, (ushort)pk.Waza4.WazaId] | |
| : GetCurrentMoves(pk.DevId, pk.FormId, pk.Level, false); | |
| // Type reducing berries | |
| var type1 = tera && pk.GemType != GemType.NIJI ? (byte)(pk.GemType - 2) : pi.Type1; | |
| var type2 = tera && pk.GemType != GemType.NIJI ? (byte)(pk.GemType - 2) : pi.Type2; | |
| var weaknesses = GetTypeWeaknesses(type1, type2, ability); | |
| var immunities = GetTypeImmunities(type1, type2, ability); | |
| bool poison = type1 == (byte)TypeIndex.Poison || type2 == (byte)TypeIndex.Poison; | |
| // Color Change: Pokémon's type can change depending on the move it was last hit by | |
| if (ability == 016) | |
| { | |
| for (byte type = 0; type <= 17; type++) | |
| { | |
| var weak = GetTypeWeaknesses(type, type, ability); | |
| weaknesses = weaknesses.Concat(weak).ToList(); | |
| } | |
| } | |
| // Protean, Libero: Pokémon's type can change depending on the moves it uses | |
| if (ability is 168 or 236) | |
| { | |
| foreach (var m in moves) | |
| { | |
| var type = GetMoveType(m); | |
| var weak = GetTypeWeaknesses(type, type, ability); | |
| weaknesses = weaknesses.Concat(weak).ToList(); | |
| } | |
| } | |
| // Mimicry: Pokémon's type can change depending on the current terrain | |
| if (ability == 250 && moves.Any(z => z is 580 or 581 or 604 or 678)) | |
| { | |
| var g = GetTypeWeaknesses((byte)TypeIndex.Grass, (byte)TypeIndex.Grass, ability); | |
| var m = GetTypeWeaknesses((byte)TypeIndex.Fairy, (byte)TypeIndex.Fairy, ability); | |
| var e = GetTypeWeaknesses((byte)TypeIndex.Electric, (byte)TypeIndex.Electric, ability); | |
| var p = GetTypeWeaknesses((byte)TypeIndex.Psychic, (byte)TypeIndex.Psychic, ability); | |
| weaknesses = weaknesses.Concat(g).Concat(m).Concat(e).Concat(p).ToList(); | |
| } | |
| // Weakness Policy, if the Pokémon has at least 1 weakness | |
| if (weaknesses.Count != 0) | |
| PotentialHeldItems.Add(0639); // Weakness Policy | |
| // Conversion: Pokémon's type can change depending on the type of its first move | |
| // Conversion uses the move's original type, ignoring any ability modifiers (e.g. Refrigerate), type modifiers (e.g. Revelation Dance), or form modifiers (e.g. Raging Bull) | |
| // Conversion fails if the resulting type matches one of its current types | |
| // Conversion fails if the user is Terastallized, even into Stellar | |
| if (moves.Contains(160) && !tera) | |
| { | |
| var conversionType = GetMoveType(moves[0]); | |
| if (type1 != conversionType && type2 != conversionType) | |
| { | |
| var conversionWeaknesses = GetTypeWeaknesses(conversionType, conversionType, ability); | |
| weaknesses = weaknesses.Concat(conversionWeaknesses).ToList(); | |
| } | |
| } | |
| foreach (var weakness in weaknesses) | |
| { | |
| var weaknessItems = GetTypeWeaknessItems((byte)weakness); | |
| foreach (var t in weaknessItems) | |
| PotentialHeldItems.Add(t); | |
| } | |
| // Type boosting items | |
| var damagingMoveTypes = GetDamagingMoveTypes(moves, pk, pi, ability, type1, tera); | |
| foreach (var type in damagingMoveTypes) | |
| { | |
| var boosts = GetTypeBoostItems(type); | |
| foreach (var b in boosts) | |
| PotentialHeldItems.Add(b); | |
| } | |
| // Air Balloon, if the Pokémon is not immune to Ground-type moves | |
| if (!immunities.Contains((byte)TypeIndex.Ground)) | |
| PotentialHeldItems.Add(0541); | |
| // Physical move related items | |
| var numPhysicalMoves = moves.Where(z => GetMoveIsPhysical(z, baseStats, iv, ev, level, nature, ability)).Count(); | |
| if (numPhysicalMoves >= 1) | |
| { | |
| PotentialHeldItems.Add(0201); // Liechi Berry | |
| PotentialHeldItems.Add(0220); // Choice Band | |
| PotentialHeldItems.Add(0266); // Muscle Band | |
| // Toxic Orb, if ability is Toxic Boost | |
| if (ability == 137) | |
| PotentialHeldItems.Add(0272); | |
| // make Choice Band and Metronome more prevalent if ability is Gorilla Tactics | |
| if (ability == 255) | |
| { | |
| for (int i = 0; i < 10; i++) | |
| { | |
| PotentialHeldItems.Add(0220); // Choice Band | |
| PotentialHeldItems.Add(0277); // Metronome | |
| } | |
| } | |
| } | |
| // Special move related items | |
| var numSpecialMoves = moves.Where(z => GetMoveIsSpecial(z, baseStats, iv, ev, level, nature, ability)).Count(); | |
| if (numSpecialMoves >= 1) | |
| { | |
| PotentialHeldItems.Add(0204); // Petaya Berry | |
| PotentialHeldItems.Add(0267); // Wise Glasses | |
| PotentialHeldItems.Add(0297); // Choice Specs | |
| // Flame Orb, if ability is Flare Boost | |
| if (ability == 138) | |
| PotentialHeldItems.Add(0273); | |
| } | |
| // Damaging moves | |
| var numDamagingMoves = moves.Where(z => !GetMoveIsStatus(z)).Count(); | |
| if (numDamagingMoves >= 1) | |
| { | |
| PotentialHeldItems.Add(0232); // Scope Lens | |
| PotentialHeldItems.Add(0253); // Shell Bell | |
| PotentialHeldItems.Add(0268); // Expert Belt | |
| PotentialHeldItems.Add(0270); // Life Orb | |
| PotentialHeldItems.Add(0277); // Metronome | |
| PotentialHeldItems.Add(0326); // Razor Claw | |
| // Remove Expert Belt if the only damaging move type is Normal | |
| if (damagingMoveTypes.Count == 1 && damagingMoveTypes.Contains((byte)TypeIndex.Normal)) | |
| PotentialHeldItems.Remove(0268); // Expert Belt | |
| } | |
| // Assault Vest, if at least 2 damaging moves are present | |
| if (numDamagingMoves >= 2) | |
| PotentialHeldItems.Add(0640); | |
| // Wide Lens, Zoom Lens, and Blunder Policy if it has at least 1 move that can miss, that also isn't a one-hit KO move | |
| if (moves.Any(z => GetMoveAccuracy(z, baseStats, iv, ev, level, nature, ability, poison) < 100 && !IsMoveOneHitKO(z))) | |
| { | |
| PotentialHeldItems.Add(0265); // Wide Lens | |
| PotentialHeldItems.Add(0276); // Zoom Lens | |
| PotentialHeldItems.Add(1121); // Blunder Policy | |
| } | |
| // King's Rock and Razor Fang, if ability is not Sheer Force and it has a move with a non-guaranteed chance to flinch, or if ability is Sheer Force and it has at least 1 damaging move | |
| if ((ability != 125 && moves.Any(z => GetMoveCanFlinch(z))) || (ability == 125 && numDamagingMoves >= 1)) | |
| { | |
| PotentialHeldItems.Add(0221); // King's Rock | |
| PotentialHeldItems.Add(0327); // Razor Fang | |
| } | |
| // Focus Sash, if ability is not Sturdy | |
| if (ability != 005) | |
| PotentialHeldItems.Add(0275); | |
| // Mental Herb and Destiny Knot, if the Pokémon is not gender unknown and ability is not Oblivious or Aroma Veil | |
| if (gender != SexGroup.UNKNOWN && ability is not (012 or 165)) | |
| { | |
| PotentialHeldItems.Add(0219); // Mental Herb | |
| PotentialHeldItems.Add(0280); // Destiny Knot | |
| } | |
| // Eviolite, if the Pokémon is capable of evolution, but only at Lv. 20 or higher for balance | |
| if (pi.Evolutions.Count != 0 && pk.Level >= 20) | |
| { | |
| for (int i = 0; i < 10; i++) | |
| PotentialHeldItems.Add(0538); | |
| } | |
| // Black Sludge, if the Pokémon is Poison-type | |
| if (poison) | |
| PotentialHeldItems.Add(0281); | |
| // Species-specific items | |
| var specific = GetSpeciesSpecificHeldItem(pk.DevId, pk.FormId); | |
| if (specific != 0 && CanUtilizeSpeciesSpecificHeldItem(pk.DevId, pk.FormId, damagingMoveTypes)) | |
| { | |
| for (int i = 0; i < 10; i++) | |
| PotentialHeldItems.Add((ushort)specific); | |
| } | |
| // Chesto Berry, if the Pokémon has Rest and ability is not Insomnia, Vital Spirit, Sweet Veil, Comatose, or Purifying Salt | |
| if (moves.Contains(156) && ability is not (015 or 072 or 175 or 213 or 272)) | |
| { | |
| for (int i = 0; i < 10; i++) | |
| PotentialHeldItems.Add(0150); | |
| } | |
| // Persim Berry, if the Pokémon has Thrash, Petal Dance, Outrage, or Raging Fury, and ability is not Own Tempo | |
| if (moves.Any(z => IsMoveRampaging(z)) && ability != 020) | |
| { | |
| for (int i = 0; i < 10; i++) | |
| PotentialHeldItems.Add(0156); | |
| } | |
| // Trick, Switcheroo | |
| if (moves.Any(z => z is 271 or 415)) | |
| { | |
| PotentialHeldItems.Add(0272); // Toxic Orb | |
| PotentialHeldItems.Add(0273); // Flame Orb | |
| PotentialHeldItems.Add(0279); // Lagging Tail | |
| PotentialHeldItems.Add(0543); // Ring Target | |
| } | |
| // Trick Room: Iron Ball | |
| if (moves.Contains(433)) | |
| PotentialHeldItems.Add(0278); | |
| // Facade, Psycho Shift, Guts, Marvel Scale, Quick Feet | |
| if (moves.Any(z => z is 263 or 375) || ability is 062 or 063 or 095) | |
| { | |
| PotentialHeldItems.Add(0272); // Toxic Orb | |
| PotentialHeldItems.Add(0273); // Flame Orb | |
| } | |
| // Light Screen, Reflect, Aurora Veil: Light Clay | |
| if (moves.Any(z => z is 113 or 115 or 694)) | |
| PotentialHeldItems.Add(0269); | |
| // Weather | |
| if (moves.Any(z => z is 881 or 883) || ability == 117) PotentialHeldItems.Add(0282); // Icy Rock | |
| if (moves.Contains(201) || ability is 045 or 245) PotentialHeldItems.Add(0283); // Smooth Rock | |
| if (moves.Contains(241) || ability is 070 or 288) PotentialHeldItems.Add(0284); // Heat Rock | |
| if (moves.Contains(240) || ability == 002) PotentialHeldItems.Add(0285); // Damp Rock | |
| // Acrobatics: Normal Gem or no held item, depending on move type | |
| if (moves.Contains(512)) | |
| { | |
| bool normal = (GetMoveType(512) == (byte)TypeIndex.Normal && !IsAbilityChangeNormalTypeMoves(ability)) || ability == 096; | |
| var acrobaticsItem = normal ? 0564 : 0; | |
| for (int i = 0; i < 10; i++) | |
| PotentialHeldItems.Add((ushort)acrobaticsItem); | |
| } | |
| // Binding moves | |
| if (moves.Any(z => GetMoveIsBinding(z))) | |
| { | |
| PotentialHeldItems.Add(0286); // Grip Claw | |
| PotentialHeldItems.Add(0544); // Binding Band | |
| } | |
| // Moves that restore HP: Big Root | |
| if (moves.Any(z => GetMoveIsHeal(z))) | |
| PotentialHeldItems.Add(0296); | |
| // Magic Guard: Sticky Barb | |
| if (ability == 098) | |
| PotentialHeldItems.Add(0288); | |
| // Magic Guard, Sheer Force: Life Orb | |
| if (ability is 098 or 125) | |
| { | |
| for (int i = 0; i < 10; i++) | |
| PotentialHeldItems.Add(0270); | |
| } | |
| // Safety Goggles, if ability is not Overcoat | |
| if (ability != 142) | |
| PotentialHeldItems.Add(0650); | |
| // Protective Pads, if ability is not Long Reach | |
| if (moves.Any(z => GetMoveIsContact(z)) && ability != 203) | |
| PotentialHeldItems.Add(0880); | |
| // Grassy Terrain, Grassy Surge | |
| if (moves.Contains(580) || ability == 229) | |
| { | |
| PotentialHeldItems.Add(0879); // Terrain Extender | |
| PotentialHeldItems.Add(0884); // Grassy Seed | |
| } | |
| // Misty Terrain, Misty Surge | |
| if (moves.Contains(581) || ability == 228) | |
| { | |
| PotentialHeldItems.Add(0879); // Terrain Extender | |
| PotentialHeldItems.Add(0883); // Misty Seed | |
| } | |
| // Electric Terrain, Electric Surge, Hadron Engine | |
| if (moves.Contains(604) || ability is 226 or 289) | |
| { | |
| PotentialHeldItems.Add(0879); // Terrain Extender | |
| PotentialHeldItems.Add(0881); // Electric Seed | |
| } | |
| // Psychic Terrain, Psychic Surge | |
| if (moves.Contains(678) || ability == 227) | |
| { | |
| PotentialHeldItems.Add(0879); // Terrain Extender | |
| PotentialHeldItems.Add(0882); // Psychic Seed | |
| } | |
| // Throat Spray | |
| if (moves.Any(z => GetMoveIsSound(z)) && moves.Any(z => GetMoveIsSpecial(z, baseStats, iv, ev, level, nature, ability))) | |
| { | |
| PotentialHeldItems.Add(1118); | |
| // make more prevalent if the Pokémon has Liquid Voice or Punk Rock | |
| if (ability is 204 or 244) | |
| { | |
| for (int i = 0; i < 10; i++) | |
| PotentialHeldItems.Add(1118); | |
| } | |
| } | |
| // Punching Glove | |
| if (moves.Any(z => GetMoveIsPunch(z))) | |
| { | |
| PotentialHeldItems.Add(1884); | |
| // make more prevalent if the Pokémon has Iron Fist | |
| if (ability == 089) | |
| { | |
| for (int i = 0; i < 10; i++) | |
| PotentialHeldItems.Add(1884); | |
| } | |
| } | |
| // Loaded Dice, if ability is not Skill Link | |
| if (moves.Any(z => GetMoveIsMultistrike(z)) && ability != 092) | |
| PotentialHeldItems.Add(1886); | |
| // Power Herb | |
| if (moves.Any(z => GetMoveIsTwoTurn(z))) | |
| PotentialHeldItems.Add(0271); | |
| // Flavor berries | |
| if ((byte)(nature - 1) is not (05 or 10 or 15 or 20)) PotentialHeldItems.Add(0159); // Figy Berry (-Atk) | |
| if ((byte)(nature - 1) is not (03 or 08 or 13 or 23)) PotentialHeldItems.Add(0160); // Wiki Berry (-SpA) | |
| if ((byte)(nature - 1) is not (02 or 07 or 17 or 22)) PotentialHeldItems.Add(0161); // Mago Berry (-Spe) | |
| if ((byte)(nature - 1) is not (04 or 09 or 14 or 19)) PotentialHeldItems.Add(0162); // Aguav Berry (-SpD) | |
| if ((byte)(nature - 1) is not (01 or 11 or 16 or 21)) PotentialHeldItems.Add(0163); // Iapapa Berry (-Def) | |
| // Protosynthesis, Quark Drive: Booster Energy | |
| if (ability is 281 or 282) | |
| { | |
| for (int i = 0; i < 10; i++) | |
| PotentialHeldItems.Add(1880); | |
| } | |
| // Eject Button, Eject Pack, and Heavy-Duty Boots, if the trainer is capable of sending in more Pokémon | |
| if (battleType == BattleType.SINGLE && partyCount > 1 || battleType == BattleType.DOUBLE && partyCount > 2) | |
| { | |
| PotentialHeldItems.Add(0547); // Eject Button | |
| PotentialHeldItems.Add(1119); // Eject Pack | |
| PotentialHeldItems.Add(1120); // Heavy-Duty Boots | |
| // make Eject Pack more prevalent if the Pokémon has a move that lowers its own stats, depending on if it has Contrary or not | |
| bool lower = (ability != 126 && moves.Any(z => GetMoveSecondaryEffect100PercentNegative(z, pk.GemType, tera))) | |
| || (ability == 126 && moves.Any(z => GetMoveSecondaryEffect100PercentPositive(z))); | |
| if (lower) | |
| { | |
| for (int i = 0; i < 10; i++) | |
| PotentialHeldItems.Add(1119); | |
| } | |
| // make Heavy-Duty Boots more prevalent if the Pokémon is weak to Rock | |
| if (weaknesses.Contains((byte)TypeIndex.Rock)) | |
| { | |
| for (int i = 0; i < 10; i++) | |
| PotentialHeldItems.Add(1120); | |
| } | |
| } | |
| // Contrary: remove all items that raise stats | |
| if (ability == 126) | |
| PotentialHeldItems.RemoveAll(z => StatRaisingItems.Contains(z)); | |
| PotentialHeldItems = PotentialHeldItems.Concat(GeneralPurposeHeldItems).OrderBy(z => z).ToList(); | |
| var item = PotentialHeldItems[GetRandom(PotentialHeldItems.Count)]; | |
| return (ItemID)item; | |
| } | |
| private static List<byte> GetDamagingMoveTypes(List<ushort> moves, PokeDataBattle pk, PersonalInfo pi, ushort ability, byte primaryType, bool tera) | |
| { | |
| var species = pi.Info.SpeciesNational; | |
| var form = pi.Info.Form; | |
| var gem = pk.GemType; | |
| var damagingMoveTypes = moves.Where(z => !GetMoveIsStatus(z) && !GetMoveIsUnaffectedBySTAB(z)).Select(z => GetMoveTypeAfterModifiers(species, form, z, ability, primaryType, gem, tera)).ToList(); | |
| // Weather Ball: type changes depending on the current weather | |
| if (moves.Contains(311)) | |
| { | |
| // Snowscape, Chilly Reception, Snow Warning | |
| if (moves.Any(z => z is 881 or 883) || ability == 117) | |
| damagingMoveTypes.Add((byte)TypeIndex.Ice); | |
| // Sandstorm, Sand Stream, Sand Spit | |
| if (moves.Contains(201) || ability is 045 or 245) | |
| damagingMoveTypes.Add((byte)TypeIndex.Rock); | |
| // Sunny Day, Drought, Desolate Land, Orichalcum Pulse | |
| if (moves.Contains(241) || ability is 070 or 190 or 288) | |
| damagingMoveTypes.Add((byte)TypeIndex.Fire); | |
| // Rain Dance, Drizzle, Primordial Sea | |
| if (moves.Contains(240) || ability is 002 or 189) | |
| damagingMoveTypes.Add((byte)TypeIndex.Water); | |
| // Primordial Sea and Desolate Land persist until the Pokémon is not on the field and can't be overridden by weather setting moves | |
| // if the Pokémon's only move of a given type is Weather Ball, then always assume either Water or Fire depending on the ability, regardless of its other moves | |
| if (ability is 189 or 190) | |
| { | |
| var types = new[] { GetMoveType(311), (byte)TypeIndex.Ice, (byte)TypeIndex.Rock, (ability == 189 ? (byte)TypeIndex.Water : (byte)TypeIndex.Fire) }; | |
| foreach (var t in types) | |
| { | |
| if (damagingMoveTypes.Where(z => z == t).Count() == 1) | |
| damagingMoveTypes.Remove(t); | |
| } | |
| } | |
| } | |
| // Terrain Pulse: type changes depending on the current terrain | |
| if (moves.Contains(805)) | |
| { | |
| // Grassy Terrain, Grassy Surge | |
| if (moves.Contains(580) || ability == 229) | |
| damagingMoveTypes.Add((byte)TypeIndex.Grass); | |
| // Misty Terrain, Misty Surge | |
| if (moves.Contains(581) || ability == 228) | |
| damagingMoveTypes.Add((byte)TypeIndex.Fairy); | |
| // Electric Terrain, Electric Surge, Hadron Engine | |
| if (moves.Contains(604) || ability is 226 or 289) | |
| damagingMoveTypes.Add((byte)TypeIndex.Electric); | |
| // Psychic Terrain, Psychic Surge | |
| if (moves.Contains(678) || ability == 227) | |
| damagingMoveTypes.Add((byte)TypeIndex.Psychic); | |
| } | |
| // Aura Wheel: type changes depending on Morpeko's current form, but Morpeko's form changes every turn if it has Hunger Switch | |
| if (moves.Contains(783) && species == 0877 && ability == 258) | |
| { | |
| if (form == 0) damagingMoveTypes.Add((byte)TypeIndex.Dark); | |
| if (form == 1) damagingMoveTypes.Add((byte)TypeIndex.Electric); | |
| } | |
| if (ability == 189) damagingMoveTypes.RemoveAll(z => z == (byte)TypeIndex.Fire); // Fire-type moves fizzle out in heavy rain | |
| if (ability == 190) damagingMoveTypes.RemoveAll(z => z == (byte)TypeIndex.Water); // Water-type moves evaporate in extremely harsh sunlight | |
| damagingMoveTypes = damagingMoveTypes.Distinct().OrderBy(z => z).ToList(); | |
| return damagingMoveTypes; | |
| } | |
| private static List<int> GetTypeWeaknesses(byte type1, byte type2, ushort ability) | |
| { | |
| List<int> Result = []; | |
| const int frequency = 20; | |
| for (byte i = 0; i < 18; i++) | |
| { | |
| var matchup = GetTypeMatchupResult(i, type1, type2, ability); | |
| if (matchup >= 2) | |
| { | |
| Result.Add(i); | |
| // if it is a 4x weakness, this item should be more prevalent | |
| if (matchup >= 4) | |
| { | |
| for (int f = 0; f < frequency; f++) | |
| Result.Add(i); | |
| } | |
| } | |
| } | |
| return Result; | |
| } | |
| private static List<int> GetTypeImmunities(byte type1, byte type2, ushort ability) | |
| { | |
| List<int> Result = []; | |
| for (byte i = 0; i < 18; i++) | |
| { | |
| var matchup = GetTypeMatchupResult(i, type1, type2, ability); | |
| if (matchup == 0) | |
| Result.Add(i); | |
| } | |
| return Result; | |
| } | |
| private static float GetTypeMatchupResult(byte typeAttacker, byte typeDefender1, byte typeDefender2, ushort ability) | |
| { | |
| var effect1 = GetTypeMatchupIndividual(typeAttacker, typeDefender1, ability); | |
| var effect2 = GetTypeMatchupIndividual(typeAttacker, typeDefender2, ability); | |
| var result = typeDefender1 == typeDefender2 ? effect1 : effect1 * effect2; | |
| if (IsImmuneFromAbility(typeAttacker, ability)) | |
| return 0; | |
| // Wonder Guard | |
| if (ability == 025 && result < 2) | |
| return 0; | |
| // Thick Fat | |
| if (ability == 047 && typeAttacker is (byte)TypeIndex.Fire or (byte)TypeIndex.Ice) | |
| result *= 0.50f; | |
| // Heatproof, Water Bubble | |
| if (ability is 085 or 199 && typeAttacker == (byte)TypeIndex.Fire) | |
| result *= 0.50f; | |
| // Dry Skin | |
| if (ability == 087 && typeAttacker == (byte)TypeIndex.Fire) | |
| result *= 1.25f; | |
| // Filter, Solid Rock, Prism Armor | |
| if (ability is 111 or 116 or 232 && result >= 2) | |
| result *= 0.75f; | |
| // Fluffy | |
| if (ability == 218 && typeAttacker == (byte)TypeIndex.Fire) | |
| result *= 2.00f; | |
| // Purifying Salt | |
| if (ability == 272 && typeAttacker == (byte)TypeIndex.Ghost) | |
| result *= 0.50f; | |
| return result; | |
| } | |
| private static float GetTypeMatchupIndividual(byte typeAttacker, byte typeDefender, ushort ability) | |
| { | |
| var effect = TypeChart[typeAttacker][typeDefender]; | |
| if (ability == 191 && typeDefender == (byte)TypeIndex.Flying && effect == 2) // Delta Stream | |
| return 1; | |
| return effect; | |
| } | |
| private static ushort[] GetTypeWeaknessItems(byte type) => type switch | |
| { | |
| (byte)TypeIndex.Normal => [0200], // Chilan Berry | |
| (byte)TypeIndex.Fighting => [0189], // Chople Berry | |
| (byte)TypeIndex.Flying => [0192], // Coba Berry | |
| (byte)TypeIndex.Poison => [0190], // Kebia Berry | |
| (byte)TypeIndex.Ground => [0191], // Shuca Berry | |
| (byte)TypeIndex.Rock => [0195], // Charti Berry | |
| (byte)TypeIndex.Bug => [0194], // Tanga Berry | |
| (byte)TypeIndex.Ghost => [0196], // Kasib Berry | |
| (byte)TypeIndex.Steel => [0199], // Babiri Berry | |
| (byte)TypeIndex.Fire => [0184], // Occa Berry | |
| (byte)TypeIndex.Grass => [0187], // Rindo Berry | |
| (byte)TypeIndex.Psychic => [0193], // Payapa Berry | |
| (byte)TypeIndex.Dragon => [0197], // Haban Berry | |
| (byte)TypeIndex.Dark => [0198], // Colbur Berry | |
| (byte)TypeIndex.Fairy => [0686], // Roseli Berry | |
| (byte)TypeIndex.Electric => [0186, 0546], // Wacan Berry, Cell Battery | |
| (byte)TypeIndex.Ice => [0188, 0649], // Yache Berry, Snowball | |
| (byte)TypeIndex.Water => [0185, 0545, 0648], // Passho Berry, Absorb Bulb, Luminous Moss | |
| _ => throw new ArgumentException(nameof(type)), | |
| }; | |
| private static ushort[] GetTypeBoostItems(byte type) => type switch | |
| { | |
| (byte)TypeIndex.Normal => [0251, 0564], // Silk Scarf, Normal Gem | |
| (byte)TypeIndex.Fighting => [0241, 0303], // Black Belt, Fist Plate | |
| (byte)TypeIndex.Flying => [0244, 0306], // Sharp Beak, Sky Plate | |
| (byte)TypeIndex.Poison => [0245, 0304], // Poison Barb, Toxic Plate | |
| (byte)TypeIndex.Ground => [0237, 0305], // Soft Sand, Earth Plate | |
| (byte)TypeIndex.Rock => [0238, 0309], // Hard Stone, Stone Plate | |
| (byte)TypeIndex.Bug => [0222, 0308], // Silver Powder, Insect Plate | |
| (byte)TypeIndex.Ghost => [0247, 0310], // Spell Tag, Spooky Plate | |
| (byte)TypeIndex.Steel => [0233, 0313], // Metal Coat, Iron Plate | |
| (byte)TypeIndex.Fire => [0249, 0298], // Charcoal, Flame Plate | |
| (byte)TypeIndex.Water => [0243, 0299], // Mystic Water, Splash Plate | |
| (byte)TypeIndex.Grass => [0239, 0301], // Miracle Seed, Meadow Plate | |
| (byte)TypeIndex.Electric => [0242, 0300], // Magnet, Zap Plate | |
| (byte)TypeIndex.Psychic => [0248, 0307], // Twisted Spoon, Mind Plate | |
| (byte)TypeIndex.Ice => [0246, 0302], // Never-Melt Ice, Icicle Plate | |
| (byte)TypeIndex.Dragon => [0250, 0311], // Dragon Fang, Draco Plate | |
| (byte)TypeIndex.Dark => [0240, 0312], // Black Glasses, Dread Plate | |
| (byte)TypeIndex.Fairy => [2401, 0644], // Fairy Feather, Pixie Plate | |
| _ => throw new ArgumentException(nameof(type)), | |
| }; | |
| private static ItemID GetSpeciesSpecificHeldItem(DevID dev, short form) => GetNationalDexID((ushort)dev) switch | |
| { | |
| 0025 => ItemID.ITEMID_DENKIDAMA, // Light Ball | |
| 0380 or 0381 => ItemID.ITEMID_KOKORONOSIZUKU, // Soul Dew | |
| 0483 when form == 0 => ItemID.ITEMID_KONGOUDAMA, // Adamant Orb | |
| 0484 when form == 0 => ItemID.ITEMID_SIRATAMA, // Lustrous Orb | |
| 0487 when form == 0 => ItemID.ITEMID_HAKKINDAMA, // Griseous Orb | |
| _ => ItemID.ITEMID_NONE, | |
| }; | |
| private static bool CanUtilizeSpeciesSpecificHeldItem(DevID dev, short form, List<byte> types) => GetNationalDexID((ushort)dev) switch | |
| { | |
| 0025 => true, // Light Ball boosts Attack and Sp. Attack, and is not move type specific | |
| 0380 or 0381 when types.Contains((byte)TypeIndex.Dragon) || types.Contains((byte)TypeIndex.Psychic) => true, | |
| 0483 when form == 0 && types.Contains((byte)TypeIndex.Steel) || types.Contains((byte)TypeIndex.Dragon) => true, | |
| 0484 when form == 0 && types.Contains((byte)TypeIndex.Water) || types.Contains((byte)TypeIndex.Dragon) => true, | |
| 0487 when form == 0 && types.Contains((byte)TypeIndex.Ghost) || types.Contains((byte)TypeIndex.Dragon) => true, | |
| _ => false, | |
| }; | |
| private static bool IsImmuneFromAbility(byte type, ushort ability) => ability switch | |
| { | |
| 010 or 031 or 078 when type == (byte)TypeIndex.Electric => true, // Volt Absorb, Lightning Rod, Motor Drive | |
| 011 or 087 or 114 or 190 when type == (byte)TypeIndex.Water => true, // Water Absorb, Dry Skin, Storm Drain, Desolate Land | |
| 018 or 189 or 273 when type == (byte)TypeIndex.Fire => true, // Flash Fire, Primordial Sea, Well-Baked Body | |
| 026 or 297 when type == (byte)TypeIndex.Ground => true, // Levitate, Earth Eater | |
| 157 when type == (byte)TypeIndex.Grass => true, // Sap Sipper | |
| _ => false, | |
| }; | |
| private static void RandomizeFieldItems() | |
| { | |
| "Randomizing field items...".Dump(); | |
| var fieldItems = new[] | |
| { | |
| "world/scene/parts/field/streaming_event/world_item_/world_item_0.trscn", | |
| "world/scene/parts/field/streaming_event/su1_world_item_/su1_world_item_0.trscn", | |
| "world/scene/parts/field/streaming_event/su2_world_item_/su2_world_item_0.trscn", | |
| "world/scene/parts/field/room/a_w23_field/a_w23_field_event/a_w23_field_item_/a_w23_field_item_0.trscn", | |
| "world/scene/parts/field/streaming_event/c01_item_/c01_item_0.trscn", | |
| "world/scene/parts/field/room/a_w23_i21/a_w23_i21_event/a_w23_i21_item_/a_w23_i21_item_0.trscn", | |
| "world/scene/parts/field/room/a_w23_d10/a_w23_d10_event/a_w23_d10_field_item_/a_w23_d10_field_item_0.trscn", | |
| }; | |
| foreach (var t in fieldItems) | |
| { | |
| var index = t.LastIndexOf("/") + 1; | |
| var f = t[index..]; | |
| var d = t[..index]; | |
| var area = fieldItems.IndexOf(t); | |
| var location = GetRegionName(area).ToUpper(); | |
| bool indigo = area is 2 or 6; | |
| List<(ulong Item, ulong Quantity)> OriginalFieldItems = []; | |
| List<string> ItemNames = []; | |
| var file = Path.Combine(PATH_MASTER, d, f); | |
| var obj = FlatBufferConverter.DeserializeFrom<TrinitySceneObjectTemplate>(file); | |
| var name = ""; | |
| foreach (var o in obj.Objects) | |
| { | |
| // get item name | |
| if (o.Type == "trinity_ObjectTemplate") | |
| { | |
| name = FlatBufferConverter.DeserializeFrom<TrinitySceneObjectTemplateData>(o.Data).ObjectTemplateName; | |
| ItemNames.Add(name); | |
| } | |
| // randomize items | |
| foreach (var s in o.SubObjects.Where(z => z.Type == "trinity_PropertySheet")) | |
| { | |
| var tps = FlatBufferConverter.DeserializeFrom<TrinityPropertySheet>(s.Data); | |
| foreach (var p in tps.Properties) | |
| { | |
| foreach (var i in p.Fields.Where(z => z.Name == "itemInfo")) | |
| { | |
| var item = i.Data.Item9.Fields.FirstOrDefault(z => z.Name == "itemNo").Data.TrinityPropertySheetField1; | |
| var quantity = i.Data.Item9.Fields.FirstOrDefault(z => z.Name == "num").Data.TrinityPropertySheetField1; | |
| OriginalFieldItems.Add((item.Value, quantity.Value)); | |
| // swap Tera Shards with other Tera Shards | |
| if (indigo && GetIsTeraShard((ushort)item.Value)) | |
| item.Value = TeraShards[GetRandom(TeraShards.Count)]; | |
| // swap TMs with other TMs | |
| else if (GetIsTechnicalMachine(item.Value)) | |
| item.Value = GetRandomTM(); | |
| else | |
| item.Value = (ulong)GetRandomItem(); | |
| // don't give excessive quantities | |
| if (ValuableItems.Contains((ushort)item.Value) || quantity.Value >= 10) | |
| quantity.Value = 1; | |
| // give Tera Shards a special, weighted quantity | |
| if (GetIsTeraShard((ushort)item.Value)) | |
| quantity.Value = GetRandomWeightedQuantity(); | |
| // some players don't want to grind, so make this accessible without needing a save editor | |
| if (name == "itemball_area01_38" && MAKE_FIRST_FIELD_ITEM_999_RARE_CANDIES) | |
| { | |
| item.Value = 0050; | |
| quantity.Value = 999; | |
| } | |
| } | |
| } | |
| s.Data = FlatBufferConverter.SerializeFrom(tps); | |
| } | |
| } | |
| if (GENERATE_SPOILER_LOG_FILE) | |
| WriteFieldItemChangesToLog(obj, OriginalFieldItems, ItemNames, area); | |
| var data = FlatBufferConverter.SerializeFrom(obj); | |
| var dest = Path.Combine(DEST_MASTER, d); | |
| if (!Directory.Exists(dest)) | |
| Directory.CreateDirectory(dest); | |
| File.WriteAllBytes(Path.Combine(dest, f), data); // Scarlet | |
| File.WriteAllBytes(Path.Combine(dest, f.Replace("_0.", "_1.")), data); // Violet | |
| } | |
| } | |
| private static ulong GetRandomWeightedQuantity() => GetRandom(100) switch | |
| { | |
| <= 99 and >= 65 => 15, // 35% | |
| <= 64 and >= 40 => 20, // 25% | |
| <= 39 and >= 20 => 25, // 20% | |
| <= 19 and >= 10 => 30, // 10% | |
| <= 9 and >= 5 => 35, // 5% | |
| <= 4 and >= 1 => 40, // 4% | |
| _ => 50, // 1% — enough to change a Pokémon's Tera Type! | |
| }; | |
| private static void RandomizeSparklingItems() | |
| { | |
| "Randomizing sparkling items...".Dump(); | |
| var sparklingItems = new[] | |
| { | |
| "world/data/item/hiddenItemDataTable/hiddenItemDataTable_array.bin", | |
| "world/data/item/hiddenItemDataTable_su1/hiddenItemDataTable_su1_array.bin", | |
| "world/data/item/hiddenItemDataTable_su2/hiddenItemDataTable_su2_array.bin", | |
| "world/data/item/hiddenItemDataTable_lc/hiddenItemDataTable_lc_array.bin", | |
| }; | |
| foreach (var h in sparklingItems) | |
| { | |
| var index = h.LastIndexOf("/") + 1; | |
| var f = h[index..]; | |
| var d = h[..index]; | |
| var region = sparklingItems.IndexOf(h); | |
| var location = GetRegionName(region).ToUpper(); | |
| SpoilerLog.Add($"== SPARKLING ITEMS ({location}) =="); | |
| var file = Path.Combine(PATH_MASTER, d, f); | |
| var obj = FlatBufferConverter.DeserializeFrom<HiddenItemDataTableArray>(file); | |
| foreach (var t in obj.Table) | |
| { | |
| var items = new[] { t.Item1, t.Item2, t.Item3, t.Item4, t.Item5, t.Item6, t.Item7, t.Item8, t.Item9, t.Item10 }; | |
| items = items.OrderByDescending(z => z.EmergePercent).ToArray(); | |
| List<(ItemID Item, int Quantity, int Rate)> OldItems = []; | |
| List<(ItemID Item, int Quantity, int Rate)> NewItems = []; | |
| foreach (var item in items.Where(z => z.ItemId != 0 && z.EmergePercent != 0)) | |
| { | |
| OldItems.Add((item.ItemId, item.DropCount, item.EmergePercent)); | |
| item.ItemId = GetRandomItem(); | |
| item.DropCount = GetIsTeraShard((ushort)item.ItemId) ? 5 : 1; | |
| NewItems.Add((item.ItemId, item.DropCount, item.EmergePercent)); | |
| } | |
| if (GENERATE_SPOILER_LOG_FILE) | |
| { | |
| var tableID = int.Parse(t.TableId); | |
| if (tableID is 1044 or 1045 or 10000000) | |
| continue; | |
| List<string> Areas = []; | |
| var key = GetSparklingItemBiomeKey(tableID); | |
| bool areaZero = tableID == 100001; | |
| foreach (var a in key) | |
| { | |
| var area = GetAreaName(a.AreaID, areaZero); | |
| var biome = GetBiomeDisplayString(a.Biome); | |
| var display = $"{area} - {biome}"; | |
| Areas.Add(display); | |
| } | |
| SpoilerLog.Add("================================================================================"); | |
| foreach (var a in Areas.Distinct()) | |
| SpoilerLog.Add($"| {a, -76} |"); | |
| SpoilerLog.Add("================================================================================"); | |
| SpoilerLog.Add("| Rate | Old Item | New Item |"); | |
| SpoilerLog.Add("================================================================================"); | |
| for (int i = 0; i < NewItems.Count; i++) | |
| { | |
| var oldItem = OldItems[i]; | |
| var newItem = NewItems[i]; | |
| var oldItemDisplay = $"{StringsItem[(ushort)oldItem.Item]} (×{oldItem.Quantity})"; | |
| var newItemDisplay = $"{StringsItem[(ushort)newItem.Item]} (×{newItem.Quantity})"; | |
| float rate = newItem.Rate / 10.0f; | |
| SpoilerLog.Add($"| {rate:00.00}% | {oldItemDisplay, -32} | {newItemDisplay, -32} |"); | |
| } | |
| SpoilerLog.Add("================================================================================"); | |
| SpoilerLog.Add(""); | |
| } | |
| } | |
| var data = FlatBufferConverter.SerializeFrom(obj); | |
| var dest = Path.Combine(DEST_MASTER, d); | |
| if (!Directory.Exists(dest)) | |
| Directory.CreateDirectory(dest); | |
| File.WriteAllBytes(Path.Combine(dest, f), data); | |
| } | |
| } | |
| private static void RandomizePickupItemTables() | |
| { | |
| SpoilerLog.Add("== PICKUP ITEMS =="); | |
| "Randomizing Pickup item tables...".Dump(); | |
| const string file = "monohiroiItemData_array.bin"; | |
| var dest = Path.Combine(DEST_MASTER, "world/data/item/monohiroiItemData/"); | |
| var path = Path.Combine(PATH_MASTER, "world/data/item/monohiroiItemData/"); | |
| var lva = GetLevelAbbreviation(LANGUAGE); | |
| var sep = GetRangeSeparator(LANGUAGE); | |
| var obj = FlatBufferConverter.DeserializeFrom<MonohiroiItemArray>(Path.Combine(path, file)); | |
| for (int i = 0; i < obj.Table.Count; i++) | |
| { | |
| var t = obj.Table[i]; | |
| var start = i == 0 ? 1 : 1 + (i * 10); | |
| var end = start + 9; | |
| SpoilerLog.Add($"{lva}{start}{sep}{end}"); | |
| var items = new[] | |
| { | |
| t.Item01, t.Item02, t.Item03, t.Item04, t.Item05, t.Item06, t.Item07, t.Item08, t.Item09, t.Item10, | |
| t.Item11, t.Item12, t.Item13, t.Item14, t.Item15, t.Item16, t.Item17, t.Item18, t.Item19, t.Item20, | |
| t.Item21, t.Item22, t.Item23, t.Item24, t.Item25, t.Item26, t.Item27, t.Item28, t.Item29, t.Item30, | |
| }; | |
| items = items.Where(z => z.ItemId != ItemID.ITEMID_NONE).OrderByDescending(z => z.Rate).ToArray(); | |
| foreach (var item in items) | |
| { | |
| item.ItemId = GetRandomItem(); | |
| SpoilerLog.Add($"- {item.Rate}% {StringsItem[(ushort)item.ItemId]}"); | |
| } | |
| SpoilerLog.Add(""); | |
| } | |
| if (!Directory.Exists(dest)) | |
| Directory.CreateDirectory(dest); | |
| var data = FlatBufferConverter.SerializeFrom(obj); | |
| File.WriteAllBytes(Path.Combine(dest, file), data); | |
| } | |
| private static void RandomizeRummagingItems() | |
| { | |
| const string file = "rummagingItemDataTable_array.bin"; | |
| var dest = Path.Combine(DEST_MASTER, "world/data/item/rummagingItemDataTable/"); | |
| var path = Path.Combine(PATH_MASTER, "world/data/item/rummagingItemDataTable/"); | |
| var obj = FlatBufferConverter.DeserializeFrom<RummagingItemDataTableArray>(Path.Combine(path, file)); | |
| foreach (var t in obj.Table) | |
| { | |
| if (t.Item00 != ItemID.ITEMID_NONE) t.Item00 = GetRandomItem(); | |
| if (t.Item01 != ItemID.ITEMID_NONE) t.Item01 = GetRandomItem(); | |
| if (t.Item02 != ItemID.ITEMID_NONE) t.Item02 = GetRandomItem(); | |
| if (t.Item03 != ItemID.ITEMID_NONE) t.Item03 = GetRandomItem(); | |
| if (t.Item04 != ItemID.ITEMID_NONE) t.Item04 = GetRandomItem(); | |
| } | |
| if (!Directory.Exists(dest)) | |
| Directory.CreateDirectory(dest); | |
| var data = FlatBufferConverter.SerializeFrom(obj); | |
| File.WriteAllBytes(Path.Combine(dest, file), data); | |
| } | |
| private static void RandomizeTeraRaidEncounters() | |
| { | |
| "Randomizing Tera Raid Battle encounters...".Dump(); | |
| var teraRaidBattlesPaldea = new[] | |
| { | |
| "world/data/raid/raid_enemy_01/raid_enemy_01_array.bin", | |
| "world/data/raid/raid_enemy_02/raid_enemy_02_array.bin", | |
| "world/data/raid/raid_enemy_03/raid_enemy_03_array.bin", | |
| "world/data/raid/raid_enemy_04/raid_enemy_04_array.bin", | |
| "world/data/raid/raid_enemy_05/raid_enemy_05_array.bin", | |
| "world/data/raid/raid_enemy_06/raid_enemy_06_array.bin", | |
| }; | |
| var teraRaidBattlesKitakami = new[] | |
| { | |
| "world/data/raid/su1_raid_enemy_01/su1_raid_enemy_01_array.bin", | |
| "world/data/raid/su1_raid_enemy_02/su1_raid_enemy_02_array.bin", | |
| "world/data/raid/su1_raid_enemy_03/su1_raid_enemy_03_array.bin", | |
| "world/data/raid/su1_raid_enemy_04/su1_raid_enemy_04_array.bin", | |
| "world/data/raid/su1_raid_enemy_05/su1_raid_enemy_05_array.bin", | |
| "world/data/raid/su1_raid_enemy_06/su1_raid_enemy_06_array.bin", | |
| }; | |
| var teraRaidBattlesBlueberry = new[] | |
| { | |
| "world/data/raid/su2_raid_enemy_01/su2_raid_enemy_01_array.bin", | |
| "world/data/raid/su2_raid_enemy_02/su2_raid_enemy_02_array.bin", | |
| "world/data/raid/su2_raid_enemy_03/su2_raid_enemy_03_array.bin", | |
| "world/data/raid/su2_raid_enemy_04/su2_raid_enemy_04_array.bin", | |
| "world/data/raid/su2_raid_enemy_05/su2_raid_enemy_05_array.bin", | |
| "world/data/raid/su2_raid_enemy_06/su2_raid_enemy_06_array.bin", | |
| }; | |
| string[][] teraRaidBattles = { teraRaidBattlesPaldea, teraRaidBattlesKitakami, teraRaidBattlesBlueberry }; | |
| foreach (var TeraRaids in teraRaidBattles) | |
| { | |
| List<(DevID Species, short Form, ItemID Item, int Stars)> OldEncounters = []; | |
| List<(DevID Species, short Form, ItemID Item, int Stars)> NewEncounters = []; | |
| var area = teraRaidBattles.IndexOf(TeraRaids); | |
| foreach (var r in TeraRaids) | |
| { | |
| var index = r.LastIndexOf("/") + 1; | |
| var f = r[index..]; | |
| var d = r[..index]; | |
| var file = Path.Combine(PATH_MASTER, d, f); | |
| var obj = FlatBufferConverter.DeserializeFrom<RaidEnemyTableArray>(file); | |
| var stars = int.Parse(f[^11..^10]); | |
| List<DevID> Used = []; | |
| foreach (var boss in obj.Table) | |
| { | |
| var b = boss.Info; | |
| var t = b.BossPokePara; | |
| var e = b.BossDesc; | |
| b.RomVer = RaidRomType.BOTH; | |
| OldEncounters.Add((t.DevId, t.FormId, t.Item, stars)); | |
| if (RANDOMIZE_POKEMON_TO_HAVE_SIMILAR_STRENGTH) | |
| { | |
| var pk = GetSimilarStrengthPokemon(t.DevId, t.FormId); | |
| t.DevId = pk.Species; | |
| t.FormId = pk.Form; | |
| } | |
| else | |
| { | |
| t.DevId = GetRandomSpecies(t.DevId); | |
| t.FormId = GetRandomForm(t.DevId); | |
| } | |
| // Terapagos can only be Stellar Tera Type, and the game does not load Stellar raid crystals | |
| while (t.DevId == DevID.DEV_KODAIGAME) | |
| { | |
| t.DevId = GetRandomSpecies(0); | |
| t.FormId = GetRandomForm(t.DevId); | |
| } | |
| // if the pool is large enough, make all raids in that tier unique | |
| if (SpeciesList.Count >= 200) | |
| { | |
| while (Used.Contains(t.DevId)) | |
| { | |
| t.DevId = GetRandomSpecies(0); | |
| t.FormId = GetRandomForm(t.DevId); | |
| } | |
| } | |
| // enforce Terastallized State so that Embody Aspect activates | |
| if (t.DevId == DevID.DEV_KAMENONI) | |
| t.FormId = (short)(t.FormId + 4); | |
| if (GIVE_OPPONENT_SMEARGLE_RANDOM_MOVESETS && t.DevId == DevID.DEV_DOOBURU) | |
| { | |
| var moves = GetRandomSmeargleMoveset(); | |
| t.WazaType = WazaType.MANUAL; | |
| t.Waza1.WazaId = moves[0]; | |
| t.Waza2.WazaId = moves[1]; | |
| t.Waza3.WazaId = moves[2]; | |
| t.Waza4.WazaId = moves[3]; | |
| } | |
| else | |
| { | |
| t.WazaType = WazaType.DEFAULT; | |
| t.Waza1 = GetCleanMoveSlot(); | |
| t.Waza2 = GetCleanMoveSlot(); | |
| t.Waza3 = GetCleanMoveSlot(); | |
| t.Waza4 = GetCleanMoveSlot(); | |
| } | |
| t.Sex = SexType.DEFAULT; | |
| t.GemType = t.DevId == DevID.DEV_KAMENONI ? GetTeraType(t.DevId, t.FormId) : GemType.RANDOM; | |
| t.Seikaku = SeikakuType.DEFAULT; | |
| t.Tokusei = GetRandomAbilitySetter(); | |
| // fix Game Freak bug where 3-Star Pachirisu has no guaranteed flawless IVs | |
| if (stars == 3 && t.TalentVnum == 0) | |
| t.TalentVnum = 2; | |
| if (GIVE_POKEMON_RANDOM_HELD_ITEMS) | |
| t.Item = GetRandomItem(); | |
| if (MODIFY_POKEMON_LEVELS) | |
| { | |
| t.Level = GetModifiedLevel(t.Level); | |
| b.CaptureLv = (sbyte)GetModifiedLevel(b.CaptureLv); | |
| } | |
| if (FORCE_FULLY_EVOLVED_POKEMON && b.CaptureLv >= FORCE_FULLY_EVOLVED_POKEMON_LEVEL) | |
| { | |
| var pk = ForceEvolveSpeciesFully(t.DevId, t.FormId); | |
| t.DevId = pk.Species; | |
| t.FormId = pk.Form; | |
| } | |
| if (IsFormHeldItemDependent(t.DevId, t.FormId)) | |
| t.Item = GetFormSpecificItem(t.DevId, t.FormId); | |
| if (GIVE_ALL_POKEMON_MAX_INDIVIDUAL_VALUES) | |
| { | |
| t.TalentType = TalentType.VALUE; | |
| t.TalentValue = FlawlessIVSpread; | |
| t.TalentVnum = 0; | |
| } | |
| // if any "use move" extra actions exist, modify the move to be Tera Blast | |
| var extra = new[] { e.ExtraAction1, e.ExtraAction2, e.ExtraAction3, e.ExtraAction4, e.ExtraAction5, e.ExtraAction6 }; | |
| if (extra.Any(z => z.Wazano != WazaID.WAZA_NULL)) | |
| { | |
| foreach (var a in extra.Where(z => IsExtraActionValid(z) && z.Action == RaidBossExtraActType.WAZA && z.Wazano != WazaID.WAZA_NULL)) | |
| a.Wazano = WazaID.WAZA_TERABAASUTO; | |
| } | |
| // pick a more fitting item table | |
| var stats = GetBaseStats(t.DevId, t.FormId); | |
| var best = GetHighestStatIndex(stats); | |
| var tableIndex = stars - 1; | |
| b.DropTableFix = TeraRaidItemTablesFixed[tableIndex][best]; | |
| b.DropTableRandom = TeraRaidItemTablesBonus[tableIndex][best]; | |
| Used.Add(t.DevId); | |
| NewEncounters.Add((t.DevId, t.FormId, t.Item, stars)); | |
| } | |
| var data = FlatBufferConverter.SerializeFrom(obj); | |
| var dest = Path.Combine(DEST_MASTER, d); | |
| if (!Directory.Exists(dest)) | |
| Directory.CreateDirectory(dest); | |
| File.WriteAllBytes(Path.Combine(dest, f), data); | |
| } | |
| if (GENERATE_SPOILER_LOG_FILE) | |
| WriteTeraRaidChangesToLog(OldEncounters, NewEncounters, area); | |
| } | |
| } | |
| private static bool IsExtraActionValid(RaidBossExtraData action) | |
| { | |
| if (action.Action == RaidBossExtraActType.NONE) | |
| return false; | |
| if (action.Value == 0) | |
| return false; | |
| if (action.Action == RaidBossExtraActType.WAZA && action.Wazano == WazaID.WAZA_NULL) | |
| return false; | |
| return true; | |
| } | |
| private static void RandomizeTechnicalMachines() | |
| { | |
| "Randomizing TMs...".Dump(); | |
| const string file = "itemdata_array.bin"; | |
| var dest = Path.Combine(DEST_MASTER, "world/data/item/itemdata/"); | |
| var path = Path.Combine(PATH_MASTER, "world/data/item/itemdata/"); | |
| var moves = GetRandomMoveList(); | |
| var ctr = 0; | |
| // keep TM171 unchanged | |
| if (DO_NOT_RANDOMIZE_TERA_BLAST_TM) | |
| { | |
| moves.Remove(851); | |
| moves.Insert(171, 851); | |
| } | |
| if (TYPE_THEMED_IMPORTANT_TRAINERS != TypeThemeRandomizer.NotTypeThemed) | |
| { | |
| var extras = moves[(MAX_TM_COUNT + 1)..].ToList(); | |
| List<(int Move, int Index)> ThemedMoves = | |
| [ | |
| // single reps | |
| (GetMoveByType(ThemedTypes[00]), GetTMIndex(021)), // TM021: Katy | |
| (GetMoveByType(ThemedTypes[01]), GetTMIndex(020)), // TM020: Brassius | |
| (GetMoveByType(ThemedTypes[02]), GetTMIndex(048)), // TM048: Iono | |
| (GetMoveByType(ThemedTypes[03]), GetTMIndex(022)), // TM022: Kofu | |
| (GetMoveByType(ThemedTypes[04]), GetTMIndex(025)), // TM025: Larry | |
| (GetMoveByType(ThemedTypes[05]), GetTMIndex(114)), // TM114: Ryme | |
| (GetMoveByType(ThemedTypes[06]), GetTMIndex(120)), // TM120: Tulip | |
| (GetMoveByType(ThemedTypes[07]), GetTMIndex(124)), // TM124: Grusha | |
| (GetMoveByType(ThemedTypes[08]), GetTMIndex(062)), // TM062: Giacomo | |
| (GetMoveByType(ThemedTypes[10]), GetTMIndex(102)), // TM102: Atticus | |
| (GetMoveByType(ThemedTypes[12]), GetTMIndex(167)), // TM167: Eri | |
| (GetMoveByType(ThemedTypes[15]), GetTMIndex(225)), // TM225: Amarys | |
| (GetMoveByType(ThemedTypes[17]), GetTMIndex(226)), // TM226: Drayton | |
| // multiple reps | |
| (GetMoveByTypeIndex(ThemedTypes[09], 0), GetTMIndex(038)), // TM038: Mela | |
| (GetMoveByTypeIndex(ThemedTypes[09], 1), GetTMIndex(207)), // TM207: Crispin | |
| (GetMoveByTypeIndex(ThemedTypes[11], 0), GetTMIndex(079)), // TM079: Ortega | |
| (GetMoveByTypeIndex(ThemedTypes[11], 1), GetTMIndex(227)), // TM227: Lacey | |
| ]; | |
| foreach (var t in ThemedMoves) | |
| moves[t.Index] = t.Move; | |
| int GetMoveByType(byte type) => extras.FirstOrDefault(z => z != 851 && GetMoveType((ushort)z) == type); | |
| int GetMoveByTypeIndex(byte type, byte index) => extras.Where(z => z != 851 && GetMoveType((ushort)z) == type).ToList()[index]; | |
| int GetTMIndex(int index) => index >= 100 ? index : index - 1; | |
| } | |
| if (!Directory.Exists(dest)) | |
| Directory.CreateDirectory(dest); | |
| var obj = FlatBufferConverter.DeserializeFrom<ItemDataArray>(Path.Combine(path, file)); | |
| foreach (var t in obj.Table) | |
| { | |
| if (ctr > MAX_TM_COUNT) | |
| { | |
| var data = FlatBufferConverter.SerializeFrom(obj); | |
| File.WriteAllBytes(Path.Combine(dest, file), data); | |
| break; | |
| } | |
| if (t.MachineWaza == WazaID.WAZA_NULL) | |
| continue; | |
| t.MachineWaza = (WazaID)moves[ctr]; | |
| TechnicalMachineMoves.Add(moves[ctr]); | |
| ctr++; | |
| } | |
| if (RANDOMIZE_POKEMON_LEARNSETS != LearnsetRandomizer.MetronomeOnly) | |
| { | |
| if (TM_COMPATIBILITY == TMCompatibilityRandomizer.FullCompatibility) | |
| { | |
| foreach (var t in PersonalInfoEdited.Table.Where(z => z.IsPresentInGame)) | |
| { | |
| t.TechnicalMachine.Clear(); | |
| foreach (var m in TechnicalMachineMoves) | |
| t.TechnicalMachine.Add((ushort)m); | |
| } | |
| UpdatePersonalTable(); | |
| } | |
| else if (TM_COMPATIBILITY is TMCompatibilityRandomizer.Randomized or TMCompatibilityRandomizer.RandomizedPreferSameType) | |
| { | |
| RandomizeTMCompatibility(); | |
| } | |
| else | |
| { | |
| FixTMCompatibility(); | |
| } | |
| } | |
| FixTMMachine(); | |
| FixTMIcons(); | |
| } | |
| private static void RandomizeTMCompatibility() | |
| { | |
| bool prefer = TM_COMPATIBILITY == TMCompatibilityRandomizer.RandomizedPreferSameType; | |
| bool follow = RANDOMIZED_TM_COMPATIBILITY_FOLLOW_EVOLUTIONS && RANDOMIZE_POKEMON_EVOLUTIONS == EvolutionRandomizer.DoNotRandomize; | |
| var chanceTypeAny = follow ? 40 : 50; | |
| var chanceTypeSame = follow ? 70 : 80; | |
| foreach (var t in PersonalInfoEdited.Table.Where(z => z.IsPresentInGame)) | |
| { | |
| t.TechnicalMachine.Clear(); | |
| foreach (var m in TechnicalMachineMoves) | |
| { | |
| var type = GetMoveType((ushort)m); | |
| bool match = prefer && (type == t.Type1 || type == t.Type2); | |
| // no evolutions, give higher rate of compatibility | |
| if (follow && t.Evolutions.Count == 0) | |
| { | |
| chanceTypeAny = 50; | |
| chanceTypeSame = 80; | |
| } | |
| if (GetRandom(100) < chanceTypeAny) | |
| t.TechnicalMachine.Add((ushort)m); | |
| if (GetRandom(100) < chanceTypeSame && match) | |
| t.TechnicalMachine.Add((ushort)m); | |
| } | |
| // if a move exists in its learnset and is also a TM, force compatibility | |
| foreach (var m in t.Learnset.Where(z => TechnicalMachineMoves.Contains(z.Move))) | |
| t.TechnicalMachine.Add(m.Move); | |
| t.TechnicalMachine = t.TechnicalMachine.Distinct().OrderBy(z => z).ToList(); | |
| } | |
| if (RANDOMIZED_TM_COMPATIBILITY_FOLLOW_EVOLUTIONS && RANDOMIZE_POKEMON_EVOLUTIONS == EvolutionRandomizer.DoNotRandomize) | |
| { | |
| foreach (var t in PersonalInfoEdited.Table.Where(z => z.IsPresentInGame && z.Evolutions.Count != 0)) | |
| { | |
| // first evo | |
| foreach (var e in t.Evolutions) | |
| { | |
| var evoS1 = e.SpeciesInternal; | |
| var evoF1 = e.Form; | |
| var evo1 = PersonalInfoEdited.Table.FirstOrDefault(z => z.Info.SpeciesInternal == evoS1 && z.Info.Form == evoF1); | |
| foreach (var m in t.TechnicalMachine) | |
| evo1.TechnicalMachine.Add(m); | |
| evo1.TechnicalMachine = evo1.TechnicalMachine.Distinct().OrderBy(z => z).ToList(); | |
| // second evo | |
| foreach (var f in evo1.Evolutions) | |
| { | |
| var evoS2 = f.SpeciesInternal; | |
| var evoF2 = f.Form; | |
| var evo2 = PersonalInfoEdited.Table.FirstOrDefault(z => z.Info.SpeciesInternal == evoS2 && z.Info.Form == evoF2); | |
| foreach (var m in evo1.TechnicalMachine) | |
| evo2.TechnicalMachine.Add(m); | |
| evo2.TechnicalMachine = evo2.TechnicalMachine.Distinct().OrderBy(z => z).ToList(); | |
| } | |
| } | |
| } | |
| } | |
| UpdatePersonalTable(); | |
| } | |
| private static void FixTMCompatibility() | |
| { | |
| var original = PersonalDumperSV.TMIndexes; | |
| foreach (var t in PersonalInfoEdited.Table.Where(z => z.IsPresentInGame)) | |
| { | |
| List<ushort> Revised = []; | |
| List<int> Indices = []; | |
| var learnable = t.TechnicalMachine; | |
| for (int i = 0; i < original.Length; i++) | |
| { | |
| if (learnable.Contains(original[i])) | |
| Indices.Add(i); | |
| } | |
| for (int i = 1; i < TechnicalMachineMoves.Count; i++) | |
| { | |
| if (!Indices.Contains(i)) | |
| continue; | |
| var index = i >= 100 ? i : i - 1; | |
| Revised.Add((ushort)TechnicalMachineMoves[index]); | |
| } | |
| t.TechnicalMachine = Revised.ToList(); | |
| // if a move exists in its learnset and is also a TM, force compatibility | |
| foreach (var m in t.Learnset.Where(z => TechnicalMachineMoves.Contains(z.Move))) | |
| t.TechnicalMachine.Add(m.Move); | |
| t.TechnicalMachine = t.TechnicalMachine.Distinct().OrderBy(z => z).ToList(); | |
| } | |
| UpdatePersonalTable(); | |
| } | |
| private static void FixTMMachine() | |
| { | |
| const string file = "shop_wazamachine_data_array.bin"; | |
| var dest = Path.Combine(DEST_MASTER, "world/data/ui/shop/shop_wazamachine/shop_wazamachine_data/"); | |
| var path = Path.Combine(PATH_MASTER, "world/data/ui/shop/shop_wazamachine/shop_wazamachine_data/"); | |
| var obj = FlatBufferConverter.DeserializeFrom<ShopWazamachineDataArray>(Path.Combine(path, file)); | |
| for (int i = 0; i < obj.Table.Count; i++) | |
| { | |
| // DLC TMs are not appended to the bottom of the table... they're placed at the start! | |
| var index = i switch | |
| { | |
| >= 000 and <= 057 => i + 172, // TM172 to TM229 | |
| >= 058 and <= 156 => i - 058, // TM001 to TM099 | |
| >= 157 and <= 228 => i - 057, // TM100 to TM171 | |
| _ => throw new ArgumentException(nameof(i)), | |
| }; | |
| // write back to table | |
| var item = obj.Table[i]; | |
| item.WazaNo = TechnicalMachineMoves[index]; | |
| if (MAKE_ALL_TMs_CRAFTABLE_FROM_START) | |
| { | |
| item.Cond = CondEnum.NONE; | |
| item.CondValue = string.Empty; | |
| } | |
| if (RANDOMIZE_TM_RECIPES) | |
| { | |
| var items = GetRandomItemList()[..3]; | |
| item.Item01 = items[0]; | |
| item.ItemNum01 = GetRandom(1, 4); | |
| item.DevNo01 = 0; | |
| if (item.Item02 != ItemID.ITEMID_NONE) | |
| { | |
| item.Item02 = items[1]; | |
| item.ItemNum02 = GetRandom(1, 4); | |
| item.DevNo02 = 0; | |
| } | |
| if (item.Item03 != ItemID.ITEMID_NONE) | |
| { | |
| item.Item03 = items[2]; | |
| item.ItemNum03 = GetRandom(1, 4); | |
| item.DevNo03 = 0; | |
| } | |
| } | |
| } | |
| if (GENERATE_SPOILER_LOG_FILE) | |
| WriteTechnicalMachineChangesToLog(obj.Table); | |
| if (!Directory.Exists(dest)) | |
| Directory.CreateDirectory(dest); | |
| var data = FlatBufferConverter.SerializeFrom(obj); | |
| File.WriteAllBytes(Path.Combine(dest, file), data); | |
| } | |
| private static void FixTMIcons() | |
| { | |
| const string file = "itemdata_array.bin"; | |
| var dest = Path.Combine(DEST_MASTER, "world/data/item/itemdata/"); | |
| var destIcon = Path.Combine(DEST_MASTER, "appli/tex/icon_item/"); | |
| var pathIcon = Path.Combine(PATH_MASTER, "appli/tex/icon_item/"); | |
| var obj = FlatBufferConverter.DeserializeFrom<ItemDataArray>(Path.Combine(dest, file)); | |
| foreach (var t in obj.Table.Where(z => z.MachineWaza != WazaID.WAZA_NULL && z.Id != 1230)) | |
| { | |
| var current = $"item_{t.Id:0000}"; | |
| var desired = GetTechnicalMachineIcon((ushort)t.MachineWaza); | |
| if (!Directory.Exists(Path.Combine(destIcon, current))) | |
| Directory.CreateDirectory(Path.Combine(destIcon, current)); | |
| File.Copy(Path.Combine(pathIcon, desired, $"{desired}.bntx"), Path.Combine(destIcon, current, $"{current}.bntx"), true); | |
| } | |
| } | |
| private static void RandomizePokemonStats() | |
| { | |
| AddRegionalFormEvolutions(); | |
| AddAlternateEvolutionMethods(); | |
| UpdatePlibConversionTable(); | |
| UpdateEvolutionParameters(); | |
| UpdateLearnsetsForRandomizer(); | |
| if (RANDOMIZE_POKEMON_ABILITIES) | |
| RandomizePokemonAbilities(); | |
| if (RANDOMIZE_POKEMON_TYPES) | |
| RandomizePokemonTypes(); | |
| if (RANDOMIZE_POKEMON_BASE_STATS != BaseStatRandomizer.DoNotRandomize) | |
| RandomizePokemonBaseStats(); | |
| if (RANDOMIZE_POKEMON_EVOLUTIONS != EvolutionRandomizer.DoNotRandomize) | |
| RandomizePokemonEvolutions(); | |
| if (RANDOMIZE_POKEMON_LEARNSETS != LearnsetRandomizer.DoNotRandomize) | |
| RandomizePokemonLearnsets(); | |
| if (REMOVE_EFFORT_VALUE_YIELDS) | |
| { | |
| var empty = new PersonalInfoStats { HP = 0, ATK = 0, DEF = 0, SPA = 0, SPD = 0, SPE = 0 }; | |
| foreach (var t in PersonalInfoEdited.Table.Where(z => z.IsPresentInGame)) | |
| t.EVYield = empty; | |
| } | |
| if (REDUCE_EGG_HATCH_CYCLES) | |
| { | |
| foreach (var t in PersonalInfoEdited.Table.Where(z => z.IsPresentInGame)) | |
| t.HatchCycles = 1; | |
| } | |
| UpdatePersonalTable(); | |
| if (GENERATE_SPOILER_LOG_FILE) | |
| WritePersonalChangesToLog(); | |
| } | |
| private static void UpdateLearnsetsForRandomizer() | |
| { | |
| // move Secret Sword from level up moves to reminder moves | |
| var ord = PersonalInfoEdited.Table.FirstOrDefault(z => z.Info.SpeciesNational == 0647 && z.Info.Form == 0); | |
| ord.Learnset.RemoveAt(ord.Learnset.IndexOf(ord.Learnset.FirstOrDefault(z => z.Move == 548))); | |
| ord.ReminderMoves.Add(548); | |
| var res = PersonalInfoEdited.Table.FirstOrDefault(z => z.Info.SpeciesNational == 0647 && z.Info.Form == 1); | |
| res.Learnset.RemoveAt(res.Learnset.IndexOf(res.Learnset.FirstOrDefault(z => z.Move == 548))); | |
| res.ReminderMoves.Add(548); | |
| // add Psyshield Bash to level up moves | |
| var sta = PersonalInfoEdited.Table.FirstOrDefault(z => z.Info.SpeciesNational == 0234); | |
| sta.Learnset.Add(new PersonalInfoMove { Move = 828, Level = 30 }); | |
| sta.Learnset = sta.Learnset.OrderBy(z => z.Level).ToList(); | |
| // add Dragon Cheer to reminder moves | |
| var dip = PersonalInfoEdited.Table.FirstOrDefault(z => z.Info.SpeciesNational == 1011); | |
| dip.ReminderMoves.Add(913); | |
| // workaround to ensure Zacian (Crowned Sword) and Zamazenta (Crowned Shield) get their signature moves instead of Iron Head | |
| if (ALLOW_BATTLE_ONLY_FORMS) | |
| { | |
| var zac = PersonalInfoEdited.Table.FirstOrDefault(z => z.Info.SpeciesNational == 0888 && z.Info.Form == 1).Learnset; | |
| var zam = PersonalInfoEdited.Table.FirstOrDefault(z => z.Info.SpeciesNational == 0889 && z.Info.Form == 1).Learnset; | |
| zac.FirstOrDefault(z => z.Move == 442).Move = 781; // Iron Head -> Behemoth Blade | |
| zam.FirstOrDefault(z => z.Move == 442).Move = 782; // Iron Head -> Behemoth Bash | |
| } | |
| // reduce level requirements for Aipom, Yanma, Girafarig, and Dunsparce | |
| if (MAKE_EVOLUTIONS_EASIER && RANDOMIZE_POKEMON_LEARNSETS == LearnsetRandomizer.DoNotRandomize) | |
| { | |
| var easy = new ushort[] { 0190, 0193, 0203, 0206 }; | |
| foreach (var t in easy) | |
| { | |
| var learn = PersonalInfoEdited.Table.FirstOrDefault(z => z.Info.SpeciesNational == t).Learnset; | |
| var moves = GetImportantSpeciesMoves(t); | |
| foreach (var m in moves) | |
| { | |
| var move = learn.FirstOrDefault(z => z.Move == m); | |
| move.Level = 30; | |
| } | |
| learn = learn.OrderBy(z => z.Level).ToList(); | |
| } | |
| } | |
| } | |
| private static void RandomizePokemonAbilities() | |
| { | |
| "Randomizing Pokémon abilities...".Dump(); | |
| foreach (var t in PersonalInfoEdited.Table.Where(z => z.IsPresentInGame)) | |
| { | |
| if (PRESERVE_FORM_CHANGE_ABILITIES && IsAbilityFormChange(t.Ability1)) | |
| continue; | |
| if (PRESERVE_OGERPON_EMBODY_ASPECT_ABILITIES && IsAbilityEmbodyAspect(t.Ability1)) | |
| continue; | |
| var species = t.Info.SpeciesNational; | |
| var form = t.Info.Form; | |
| t.Ability1 = GetRandomAbility(species, form); | |
| t.Ability2 = GetRandomAbility(species, form); | |
| t.AbilityH = GetRandomAbility(species, form); | |
| // ensure all 3 are unique | |
| while (t.Ability2 == t.Ability1 || t.Ability2 == t.AbilityH) t.Ability2 = GetRandomAbility(species, form); | |
| while (t.AbilityH == t.Ability1 || t.AbilityH == t.Ability2) t.AbilityH = GetRandomAbility(species, form); | |
| } | |
| if (RANDOM_ABILITIES_FOLLOW_EVOLUTIONS && RANDOMIZE_POKEMON_EVOLUTIONS == EvolutionRandomizer.DoNotRandomize) | |
| CopyPokemonAbilitiesToEvolutions(); | |
| } | |
| private static void RandomizePokemonTypes() | |
| { | |
| "Randomizing Pokémon types...".Dump(); | |
| foreach (var t in PersonalInfoEdited.Table.Where(z => z.IsPresentInGame && z.Info.SpeciesNational != 0493)) | |
| { | |
| var type1 = GetRandomType(); | |
| var type2 = type1; | |
| // chance at dual-type | |
| if (GetRandom(100) < RANDOM_DUAL_TYPE_CHANCE_PERCENT) | |
| { | |
| while (type2 == type1) | |
| type2 = GetRandomType(); | |
| } | |
| // if Koraidon/Miraidon is not randomized, we need to make sure they can not be a Fairy type to prevent a softlock against the final boss | |
| if (!RANDOMIZE_KORAIDON_MIRAIDON && RANDOMIZE_POKEMON_TYPES && t.Info.SpeciesNational is 1007 or 1008 && t.Info.Form == 0 && (type1 == (byte)TypeIndex.Fairy || type2 == (byte)TypeIndex.Fairy)) | |
| { | |
| if (type1 == type2) | |
| { | |
| while (type1 == (byte)TypeIndex.Fairy) | |
| { | |
| type1 = GetRandomType(); | |
| type2 = type1; | |
| } | |
| } | |
| else | |
| { | |
| while (type1 == (byte)TypeIndex.Fairy || type2 == (byte)TypeIndex.Fairy) | |
| { | |
| type1 = GetRandomType(); | |
| type2 = GetRandomType(); | |
| } | |
| } | |
| } | |
| t.Type1 = type1; | |
| t.Type2 = type2; | |
| } | |
| // keep types the same for each Ogerpon form and their respective Terastallized States | |
| for (int i = 0; i <= 3; i++) | |
| { | |
| var oger = PersonalInfoEdited.Table.FirstOrDefault(z => z.Info.SpeciesNational == 1017 && z.Info.Form == i + 0); | |
| var tera = PersonalInfoEdited.Table.FirstOrDefault(z => z.Info.SpeciesNational == 1017 && z.Info.Form == i + 4); | |
| tera.Type1 = oger.Type1; | |
| tera.Type2 = oger.Type2; | |
| } | |
| if (RANDOM_TYPES_FOLLOW_EVOLUTIONS && RANDOMIZE_POKEMON_EVOLUTIONS == EvolutionRandomizer.DoNotRandomize) | |
| CopyPokemonTypesToEvolutions(); | |
| // we only write here so that type biased learnsets can reference newly-randomized types | |
| UpdatePersonalTable(); | |
| } | |
| private static void RandomizePokemonBaseStats() | |
| { | |
| "Randomizing Pokémon base stats...".Dump(); | |
| if (RANDOMIZE_POKEMON_BASE_STATS == BaseStatRandomizer.Shuffled) | |
| RandomizePokemonBaseStatsShuffled(); | |
| if (RANDOMIZE_POKEMON_BASE_STATS == BaseStatRandomizer.WithinBST) | |
| RandomizePokemonBaseStatsWithinBST(); | |
| if (RANDOMIZE_POKEMON_BASE_STATS == BaseStatRandomizer.CompletelyRandom) | |
| RandomizePokemonBaseStatsCompletelyRandom(); | |
| // keep base stats the same for each Ogerpon form and their respective Terastallized States | |
| for (int i = 0; i <= 3; i++) | |
| { | |
| var oger = PersonalInfoEdited.Table.FirstOrDefault(z => z.Info.SpeciesNational == 1017 && z.Info.Form == i + 0); | |
| var tera = PersonalInfoEdited.Table.FirstOrDefault(z => z.Info.SpeciesNational == 1017 && z.Info.Form == i + 4); | |
| tera.Base.HP = oger.Base.HP; | |
| tera.Base.ATK = oger.Base.ATK; | |
| tera.Base.DEF = oger.Base.DEF; | |
| tera.Base.SPA = oger.Base.SPA; | |
| tera.Base.SPD = oger.Base.SPD; | |
| tera.Base.SPE = oger.Base.SPE; | |
| } | |
| } | |
| private static void RandomizePokemonBaseStatsShuffled() | |
| { | |
| foreach (var t in PersonalInfoEdited.Table.Where(z => z.IsPresentInGame)) | |
| { | |
| var stats = new[] { t.Base.HP, t.Base.ATK, t.Base.DEF, t.Base.SPA, t.Base.SPD, t.Base.SPE }; | |
| ShuffleData(stats); | |
| t.Base.HP = stats[0]; | |
| t.Base.ATK = stats[1]; | |
| t.Base.DEF = stats[2]; | |
| t.Base.SPA = stats[3]; | |
| t.Base.SPD = stats[4]; | |
| t.Base.SPE = stats[5]; | |
| } | |
| } | |
| private static void RandomizePokemonBaseStatsWithinBST() | |
| { | |
| foreach (var t in PersonalInfoEdited.Table.Where(z => z.IsPresentInGame)) | |
| { | |
| List<int> BaseStats = [ t.Base.HP, t.Base.ATK, t.Base.DEF, t.Base.SPA, t.Base.SPD, t.Base.SPE ]; | |
| List<int> Final = []; | |
| List<float> Weighted = []; | |
| var bst = BaseStats.Sum(); | |
| for (int i = 0; i < 6; i++) | |
| Weighted.Add(GetRandom(5, BaseStats[i])); | |
| float factor = Weighted.Sum() / bst; | |
| for (int i = 0; i < 6; i++) | |
| Final.Add((byte)(Weighted[i] /= factor)); | |
| // throw in some extra points to meet the original BST | |
| if (Final.Sum() < bst) | |
| { | |
| var diff = bst - Final.Sum(); | |
| for (int i = 0; i < diff; i++) | |
| { | |
| var index = GetRandom(Final.Count); | |
| Final[index]++; | |
| } | |
| } | |
| t.Base.HP = (byte)Final[0]; | |
| t.Base.ATK = (byte)Final[1]; | |
| t.Base.DEF = (byte)Final[2]; | |
| t.Base.SPA = (byte)Final[3]; | |
| t.Base.SPD = (byte)Final[4]; | |
| t.Base.SPE = (byte)Final[5]; | |
| } | |
| } | |
| private static void RandomizePokemonBaseStatsCompletelyRandom() | |
| { | |
| foreach (var t in PersonalInfoEdited.Table.Where(z => z.IsPresentInGame)) | |
| { | |
| t.Base.HP = (byte)GetRandom(5, 256); | |
| t.Base.ATK = (byte)GetRandom(5, 256); | |
| t.Base.DEF = (byte)GetRandom(5, 256); | |
| t.Base.SPA = (byte)GetRandom(5, 256); | |
| t.Base.SPD = (byte)GetRandom(5, 256); | |
| t.Base.SPE = (byte)GetRandom(5, 256); | |
| } | |
| } | |
| private static void RandomizePokemonEvolutions() | |
| { | |
| "Randomizing Pokémon evolutions...".Dump(); | |
| if (RANDOMIZE_POKEMON_EVOLUTIONS == EvolutionRandomizer.Randomize) | |
| RandomizePokemonEvolutionsStandard(); | |
| if (RANDOMIZE_POKEMON_EVOLUTIONS == EvolutionRandomizer.EvolveEveryLevel) | |
| RandomizePokemonEvolutionsEveryLevel(); | |
| } | |
| private static void RandomizePokemonEvolutionsStandard() | |
| { | |
| foreach (var t in PersonalInfoEdited.Table.Where(z => z.IsPresentInGame)) | |
| { | |
| var evos = t.Evolutions; | |
| foreach (var evo in evos) | |
| { | |
| evo.SpeciesInternal = (ushort)GetRandomSpecies(0); | |
| evo.Form = GetRandomForm((DevID)evo.SpeciesInternal); | |
| // do not evolve into same species, or into Ogerpon or Terapagos due to Tera Type limitations | |
| while (evo.SpeciesInternal == t.Info.SpeciesInternal || evo.SpeciesInternal is (ushort)DevID.DEV_KAMENONI or (ushort)DevID.DEV_KODAIGAME) | |
| { | |
| evo.SpeciesInternal = (ushort)GetRandomSpecies(0); | |
| evo.Form = GetRandomForm((DevID)evo.SpeciesInternal); | |
| } | |
| } | |
| t.EXPGrowth = (byte)RANDOM_EVOLUTION_EXP_GROWTH_RATE; // standardize growth rates | |
| } | |
| } | |
| private static void RandomizePokemonEvolutionsEveryLevel() | |
| { | |
| List<(ushort Species, ushort Form)> Possible = []; | |
| var ctr = 0; | |
| foreach (var t in PersonalInfoEdited.Table.Where(z => z.IsPresentInGame && SpeciesList.Contains((DevID)z.Info.SpeciesInternal))) | |
| { | |
| if (t.Info.SpeciesNational is 1007 or 1008 && t.Info.Form != 0 || t.Info.SpeciesNational is 1017 or 1024) | |
| continue; | |
| Possible.Add((t.Info.SpeciesInternal, t.Info.Form)); | |
| } | |
| ShuffleData(Possible); | |
| foreach (var t in PersonalInfoEdited.Table.Where(z => z.IsPresentInGame && SpeciesList.Contains((DevID)z.Info.SpeciesInternal))) | |
| { | |
| if (t.Info.SpeciesNational is 1007 or 1008 && t.Info.Form != 0 || t.Info.SpeciesNational is 1017 or 1024) | |
| continue; | |
| var evos = t.Evolutions; | |
| var evo = new PersonalInfoEvolution() | |
| { | |
| Level = 1, | |
| Method = (ushort)EvolutionType.LevelUp, | |
| Argument = 0, | |
| SpeciesInternal = Possible[ctr].Species, | |
| Form = Possible[ctr].Form, | |
| }; | |
| evos.Clear(); | |
| evos.Add(evo); | |
| ctr++; | |
| t.EXPGrowth = (byte)RANDOM_EVOLUTION_EXP_GROWTH_RATE; // standardize growth rates | |
| } | |
| } | |
| private static void RandomizePokemonLearnsets() | |
| { | |
| "Randomizing Pokémon learnsets...".Dump(); | |
| if (RANDOMIZE_POKEMON_LEARNSETS == LearnsetRandomizer.Randomize) | |
| RandomizePokemonLearnsetsStandard(); | |
| if (RANDOMIZE_POKEMON_LEARNSETS == LearnsetRandomizer.MetronomeOnly) | |
| RandomizePokemonLearnsetsMetronome(); | |
| } | |
| private static void RandomizePokemonLearnsetsStandard() | |
| { | |
| bool HasExclusiveMove(ushort species, ushort form) => species is 0491 or 0877 || species == 0720 && form == 1; | |
| bool HasImportantMove(ushort species, ushort form) => species switch | |
| { | |
| 0057 => true, // Primeape | |
| 0190 => true, // Aipom | |
| 0193 => true, // Yanma | |
| 0203 => true, // Girafarig | |
| 0206 => true, // Dunsparce | |
| 0221 => true, // Piloswine | |
| 0234 => true, // Stantler | |
| 0438 => true, // Bonsly | |
| 0648 => true, // Meloetta | |
| 0762 => true, // Steenee | |
| 0845 => true, // Cramorant | |
| 0211 when form == 1 => true, // Qwilfish (Hisuian Form) | |
| 0550 when form == 2 => true, // Basculin (White-Striped Form) | |
| _ => false, | |
| }; | |
| ushort GetExclusiveMove(ushort species, ushort form) => species switch | |
| { | |
| 0491 => 464, // Darkrai: Dark Void | |
| 0877 => 783, // Morpeko: Aura Wheel | |
| 0720 when form == 1 => 621, // Hoopa Unbound: Hyperspace Fury | |
| _ => throw new ArgumentException(nameof(species)), | |
| }; | |
| foreach (var t in PersonalInfoEdited.Table.Where(z => z.IsPresentInGame)) | |
| { | |
| var species = t.Info.SpeciesNational; | |
| var dev = t.Info.SpeciesInternal; | |
| var form = t.Info.Form; | |
| bool fire = t.Type1 == (byte)TypeIndex.Fire || t.Type2 == (byte)TypeIndex.Fire; | |
| bool electric = t.Type1 == (byte)TypeIndex.Electric || t.Type2 == (byte)TypeIndex.Electric; | |
| if (species == 0132 && DO_NOT_RANDOMIZE_DITTO_LEARNSET) continue; | |
| if (species == 0235 && DO_NOT_RANDOMIZE_SMEARGLE_LEARNSET) continue; | |
| var movesL = t.Learnset; | |
| var movesR = t.ReminderMoves; | |
| var movesE = t.EggMoves; | |
| if (STANDARDIZE_LEVELS_FOR_LEARNING_MOVES) | |
| { | |
| movesL.Clear(); | |
| for (int i = 0; i <= 100; i += 10) | |
| { | |
| var level = i == 0 ? (sbyte)1 : (sbyte)i; | |
| movesL.Add(new PersonalInfoMove { Move = 001, Level = level }); | |
| } | |
| // give all Pokémon four moves to start, just to be fair | |
| for (int i = 0; i <= 2; i++) | |
| movesL.Add(new PersonalInfoMove { Move = 001, Level = 1 }); | |
| } | |
| if (RANDOMIZE_LEARNSETS_WITH_TYPE_BIAS) | |
| { | |
| GetLearnsetWithTypeBias(movesL, dev, (byte)form); | |
| GetMoveListWithTypeBias(movesR, dev, (byte)form); | |
| GetMoveListWithTypeBias(movesE, dev, (byte)form); | |
| } | |
| else | |
| { | |
| var moves = GetRandomMoveList(); | |
| var ctr = 0; | |
| // moves only usable by a certain species/form | |
| if (HasExclusiveMove(species, form)) | |
| { | |
| var exclusive = GetExclusiveMove(species, form); | |
| moves.Add(exclusive); | |
| ShuffleData(moves); | |
| } | |
| // to avoid potential form mismatches, do not allow Secret Sword to be a level up move for Keldeo | |
| if (species == 0647) | |
| moves.Remove(548); | |
| // Burn Up is only usable by Fire types | |
| if (!fire) | |
| moves.Remove(682); | |
| // Double Shock is only usable by Electric types | |
| if (!electric) | |
| moves.Remove(892); | |
| // level up | |
| for (int i = 0; i < movesL.Count; i++) | |
| { | |
| movesL[i].Move = (ushort)moves[i]; | |
| ctr++; | |
| } | |
| // reminder | |
| for (int i = 0; i < movesR.Count; i++) | |
| { | |
| if (species == 0647 && movesR[i] == 548) continue; // keep Secret Sword for Keldeo | |
| if (species == 1011 && movesR[i] == 913) continue; // keep Dragon Cheer for Dipplin | |
| movesR[i] = (ushort)moves[ctr]; | |
| ctr++; | |
| } | |
| // egg | |
| for (int i = 0; i < movesE.Count; i++) | |
| { | |
| movesE[i] = (ushort)moves[ctr]; | |
| ctr++; | |
| } | |
| if (GUARANTEE_FOUR_MOVES_AT_LEVEL_ONE && movesL.Count(z => z.Level == 1) < 4) | |
| { | |
| while (movesL.Count(z => z.Level == 1) < 4) | |
| { | |
| movesL.Add(new PersonalInfoMove { Move = (ushort)moves[ctr], Level = 1 }); | |
| ctr++; | |
| } | |
| } | |
| } | |
| // add evolution moves and form changing moves to reminders, now that they may be inaccessible | |
| if (HasImportantMove(species, form)) | |
| AddImportantMovesToReminderMoves(species, form); | |
| movesL = movesL.OrderBy(z => z.Level).ThenBy(z => z.Move).ToList(); | |
| } | |
| // keep learnsets the same for each Ogerpon form and their respective Terastallized States | |
| for (int i = 0; i <= 3; i++) | |
| { | |
| var oger = PersonalInfoEdited.Table.FirstOrDefault(z => z.Info.SpeciesNational == 1017 && z.Info.Form == i + 0); | |
| var tera = PersonalInfoEdited.Table.FirstOrDefault(z => z.Info.SpeciesNational == 1017 && z.Info.Form == i + 4); | |
| tera.Learnset = oger.Learnset; | |
| tera.ReminderMoves = oger.ReminderMoves; | |
| tera.EggMoves = oger.EggMoves; | |
| } | |
| } | |
| private static void RandomizePokemonLearnsetsMetronome() | |
| { | |
| var metronome = new PersonalInfoMove() { Move = 118, Level = 1 }; | |
| foreach (var t in PersonalInfoEdited.Table.Where(z => z.IsPresentInGame)) | |
| { | |
| t.TechnicalMachine.Clear(); | |
| t.EggMoves.Clear(); | |
| t.ReminderMoves.Clear(); | |
| t.Learnset.Clear(); | |
| t.Learnset.Add(metronome); | |
| } | |
| } | |
| private static void AddRegionalFormEvolutions() | |
| { | |
| var regional = new[] { 0025, 0102, 0109, 0156, 0502, 0548, 0627, 0704, 0712, 0723 }; | |
| foreach (var t in regional) | |
| { | |
| var pk = PersonalInfoEdited.Table.FirstOrDefault(z => z.Info.SpeciesNational == t); | |
| var evo = new PersonalInfoEvolution() | |
| { | |
| Level = 0, | |
| Method = (ushort)EvolutionType.UseItem, | |
| Argument = 57, | |
| SpeciesInternal = (ushort)(t + 1), | |
| Form = 1, | |
| }; | |
| pk.Evolutions.Add(evo); | |
| } | |
| if (ADD_BLOODMOON_URSALUNA_EVOLUTION) | |
| { | |
| var ursaring = PersonalInfoEdited.Table.FirstOrDefault(z => z.Info.SpeciesNational == 0217); | |
| var ursaluna = PersonalInfoEdited.Table.FirstOrDefault(z => z.Info.SpeciesNational == 0901 && z.Info.Form == 1); | |
| var evo = new PersonalInfoEvolution() | |
| { | |
| Level = 0, | |
| Method = (ushort)EvolutionType.UseItem, | |
| Argument = 57, | |
| SpeciesInternal = 0901, | |
| Form = 1, | |
| }; | |
| // add evolution, change gender ratio | |
| ursaring.Evolutions.Add(evo); | |
| ursaluna.Gender = ursaring.Gender; | |
| } | |
| } | |
| private static void AddAlternateEvolutionMethods() | |
| { | |
| // add a Linking Cord variant for trade evolutions | |
| foreach (var t in PersonalInfoEdited.Table.Where(z => z.IsPresentInGame && z.Evolutions.Any(x => x.Method == (ushort)EvolutionType.Trade))) | |
| { | |
| var evo = new PersonalInfoEvolution() | |
| { | |
| Level = 0, | |
| Method = (ushort)EvolutionType.UseItem, | |
| Argument = 58, | |
| SpeciesInternal = (ushort)(t.Info.SpeciesNational + 1), | |
| Form = t.Info.Form, | |
| }; | |
| t.Evolutions.Add(evo); | |
| } | |
| // add a use item variant for trade with held item evolutions | |
| foreach (var t in PersonalInfoEdited.Table.Where(z => z.IsPresentInGame && z.Evolutions.Any(x => x.Method == (ushort)EvolutionType.TradeHeldItem))) | |
| { | |
| var template = t.Evolutions.FirstOrDefault(z => z.Method == (ushort)EvolutionType.TradeHeldItem); // finding first instance is fine, since Clamperl is not in the game | |
| var evo = new PersonalInfoEvolution() | |
| { | |
| Level = 0, | |
| Method = (ushort)EvolutionType.UseItem, | |
| Argument = template.Argument, | |
| Reserved3 = 6969, // we'll adjust this later, this is just to differentiate the two | |
| SpeciesInternal = template.SpeciesInternal, | |
| Form = template.Form, | |
| }; | |
| t.Evolutions.Add(evo); | |
| } | |
| if (REMOVE_TIME_BASED_EVOLUTIONS) | |
| { | |
| foreach (var t in PersonalInfoEdited.Table.Where(z => z.IsPresentInGame && z.Evolutions.Any(x => x.Method is (ushort)EvolutionType.LevelUpHeldItemDay or (ushort)EvolutionType.LevelUpHeldItemNight))) | |
| { | |
| var template = t.Evolutions[0]; | |
| var opposite = template.Method == (ushort)EvolutionType.LevelUpHeldItemDay ? (ushort)EvolutionType.LevelUpHeldItemNight : (ushort)EvolutionType.LevelUpHeldItemDay; | |
| var evo = new PersonalInfoEvolution() | |
| { | |
| Level = 0, | |
| Method = opposite, | |
| Argument = template.Argument, | |
| SpeciesInternal = template.SpeciesInternal, | |
| Form = template.Form, | |
| }; | |
| t.Evolutions.Add(evo); | |
| } | |
| } | |
| } | |
| private static void UpdatePlibConversionTable() | |
| { | |
| const string file = "plib_item_conversion_array.bin"; | |
| var dest = Path.Combine(DEST_MASTER, "world/data/battle/plib_item_conversion/"); | |
| var path = Path.Combine(PATH_MASTER, "world/data/battle/plib_item_conversion/"); | |
| var obj = FlatBufferConverter.DeserializeFrom<ItemTableArray>(Path.Combine(path, file)); | |
| // plib table has a hardcoded max, so need to modify empty slots | |
| for (int i = 0; i < obj.Table.Count; i++) | |
| { | |
| if (obj.Table[i].PlibID == 53) obj.Table[i].ItemID = 0221; // King's Rock | |
| if (obj.Table[i].PlibID == 54) obj.Table[i].ItemID = 0233; // Metal Coat | |
| if (obj.Table[i].PlibID == 55) obj.Table[i].ItemID = 2351; // Black Augurite | |
| if (obj.Table[i].PlibID == 56) obj.Table[i].ItemID = 2352; // Peat Block | |
| if (obj.Table[i].PlibID == 57) obj.Table[i].ItemID = 2353; // Foreign Treat | |
| if (obj.Table[i].PlibID == 58) obj.Table[i].ItemID = 2354; // Linking Cord | |
| } | |
| if (!Directory.Exists(dest)) | |
| Directory.CreateDirectory(dest); | |
| var data = FlatBufferConverter.SerializeFrom(obj); | |
| File.WriteAllBytes(Path.Combine(dest, file), data); | |
| } | |
| private static void UpdateEvolutionParameters() | |
| { | |
| var fbs = FlatBufferConverter.DeserializeFrom<ItemTableArray>(Path.Combine(DEST_MASTER, "world/data/battle/plib_item_conversion/plib_item_conversion_array.bin")); | |
| var lib = fbs.Table; | |
| foreach (var t in PersonalInfoEdited.Table.Where(z => z.IsPresentInGame)) | |
| { | |
| var species = t.Info.SpeciesNational; | |
| var dev = t.Info.SpeciesInternal; | |
| var form = t.Info.Form; | |
| var evos = t.Evolutions; | |
| for (int i = 0; i < evos.Count; i++) | |
| { | |
| var evo = evos[i]; | |
| if (evo.Method == (ushort)EvolutionType.Hisui) | |
| { | |
| if (species == 0123) | |
| { | |
| evo.Method = (ushort)EvolutionType.UseItem; | |
| evo.Argument = 55; // Black Augurite | |
| } | |
| if (species == 0217) | |
| { | |
| evo.Method = (ushort)EvolutionType.UseItem; | |
| evo.Argument = 56; // Peat Block | |
| } | |
| if (species == 0234) | |
| { | |
| evo.Method = (ushort)EvolutionType.LevelUpKnowMove; | |
| evo.Argument = 828; // Psyshield Bash | |
| } | |
| } | |
| // trade with held item | |
| if (evo.Reserved3 == 6969) | |
| { | |
| for (int p = 0; p < lib.Count; p++) | |
| { | |
| if (evo.Argument == lib[p].ItemID) | |
| { | |
| evo.Reserved3 = 0; | |
| evo.Argument = (ushort)lib[p].PlibID; | |
| } | |
| } | |
| } | |
| // version specific | |
| if (evo.Method == (ushort)EvolutionType.LevelUpVersion) | |
| { | |
| evo.Method = evo.SpeciesInternal == 0791 ? (ushort)EvolutionType.LevelUpMorning : (ushort)EvolutionType.LevelUpNight; | |
| evo.Argument = 0; | |
| } | |
| // multiplayer | |
| if (evo.Method == (ushort)EvolutionType.LevelUpUnionCircle) | |
| evo.Method = (ushort)EvolutionType.LevelUp; | |
| if (MAKE_EVOLUTIONS_EASIER && ((EvolutionType)(evo.Method)).IsLevelUpRequired()) | |
| { | |
| if (GetEvoStage(dev, (byte)form) == 1 && evo.Level > 30) evo.Level = 30; | |
| if (GetEvoStage(dev, (byte)form) == 2 && evo.Level > 40) evo.Level = 40; | |
| // Malamar | |
| if (evo.Method == (ushort)EvolutionType.LevelUpInverted) | |
| evo.Method = (ushort)EvolutionType.LevelUp; | |
| // Goodra | |
| if (evo.Method == (ushort)EvolutionType.LevelUpWeather) | |
| evo.Method = (ushort)EvolutionType.LevelUp; | |
| // Basculegion (Male) | |
| if (evo.Method == (ushort)EvolutionType.LevelUpRecoilDamageMale) | |
| { | |
| evo.Method = (ushort)EvolutionType.LevelUpMale; | |
| evo.Level = 30; | |
| evo.Argument = 0; | |
| } | |
| // Basculegion (Female) | |
| if (evo.Method == (ushort)EvolutionType.LevelUpRecoilDamageFemale) | |
| { | |
| evo.Method = (ushort)EvolutionType.LevelUpFemale; | |
| evo.Level = 30; | |
| evo.Argument = 0; | |
| } | |
| // Pawmot, Brambleghast, Rabsca | |
| if (evo.Method == (ushort)EvolutionType.LevelUpWalkStepsWith) | |
| { | |
| evo.Method = (ushort)EvolutionType.LevelUp; | |
| evo.Level = 30; | |
| evo.Argument = 0; | |
| } | |
| // Annihilape | |
| if (evo.Method == (ushort)EvolutionType.LevelUpUseMoveSpecial) | |
| { | |
| evo.Method = (ushort)EvolutionType.LevelUpKnowMove; | |
| evo.Argument = 889; | |
| } | |
| // Kingambit | |
| if (evo.Method == (ushort)EvolutionType.LevelUpDefeatEquals) | |
| { | |
| evo.Method = (ushort)EvolutionType.LevelUp; | |
| evo.Level = 40; | |
| evo.Argument = 0; | |
| } | |
| // Gholdengo | |
| if (evo.Method == (ushort)EvolutionType.LevelUpCollect999) | |
| evo.Argument = 99; | |
| } | |
| if (REMOVE_TIME_BASED_EVOLUTIONS) | |
| { | |
| // Eevee | |
| if (species == 0133 && evo.Method is (ushort)EvolutionType.LevelUpFriendshipMorning or (ushort)EvolutionType.LevelUpFriendshipNight) | |
| { | |
| evo.Argument = evo.Method == (ushort)EvolutionType.LevelUpFriendshipMorning ? (ushort)1 : (ushort)2; | |
| evo.Method = (ushort)EvolutionType.UseItem; | |
| } | |
| // Rockruff, Cosmoem | |
| if (species is 0744 or 0790 && evo.Method is (ushort)EvolutionType.LevelUpMorning or (ushort)EvolutionType.LevelUpNight) | |
| { | |
| evo.Argument = evo.Method == (ushort)EvolutionType.LevelUpMorning ? (ushort)1 : (ushort)2; | |
| evo.Method = (ushort)EvolutionType.UseItem; | |
| } | |
| // Chingling, Riolu, Snom | |
| if (evo.Method is (ushort)EvolutionType.LevelUpFriendshipMorning or (ushort)EvolutionType.LevelUpFriendshipNight) | |
| evo.Method = (ushort)EvolutionType.LevelUpFriendship; | |
| // Yungoos, Rockruff (Own Tempo), Fomantis, Greavard | |
| if (evo.Method is (ushort)EvolutionType.LevelUpMorning or (ushort)EvolutionType.LevelUpNight or (ushort)EvolutionType.LevelUpDusk) | |
| evo.Method = (ushort)EvolutionType.LevelUp; | |
| } | |
| } | |
| } | |
| } | |
| private static void CopyPokemonAbilitiesToEvolutions() | |
| { | |
| foreach (var t in PersonalInfoEdited.Table.Where(z => z.IsPresentInGame && z.Info.SpeciesNational != 0133 && z.Evolutions.Count != 0)) | |
| { | |
| // first evo | |
| foreach (var e in t.Evolutions) | |
| { | |
| var evoS1 = e.SpeciesInternal; | |
| var evoF1 = e.Form; | |
| var evo1 = PersonalInfoEdited.Table.FirstOrDefault(z => z.Info.SpeciesInternal == evoS1 && z.Info.Form == evoF1); | |
| if (PRESERVE_FORM_CHANGE_ABILITIES && IsAbilityFormChange(evo1.Ability1)) | |
| continue; | |
| evo1.Ability1 = t.Ability1; | |
| evo1.Ability2 = t.Ability2; | |
| evo1.AbilityH = t.AbilityH; | |
| // second evo | |
| foreach (var f in evo1.Evolutions) | |
| { | |
| var evoS2 = f.SpeciesInternal; | |
| var evoF2 = f.Form; | |
| var evo2 = PersonalInfoEdited.Table.FirstOrDefault(z => z.Info.SpeciesInternal == evoS2 && z.Info.Form == evoF2); | |
| if (PRESERVE_FORM_CHANGE_ABILITIES && IsAbilityFormChange(evo2.Ability1)) | |
| continue; | |
| evo2.Ability1 = t.Ability1; | |
| evo2.Ability2 = t.Ability2; | |
| evo2.AbilityH = t.AbilityH; | |
| } | |
| } | |
| } | |
| // Gimmighoul edge case | |
| var gimmi = PersonalInfoEdited.Table.FirstOrDefault(z => z.Info.SpeciesNational == 0999 && z.Info.Form == 0); | |
| var ghold = PersonalInfoEdited.Table.FirstOrDefault(z => z.Info.SpeciesNational == 1000); | |
| gimmi.Ability1 = ghold.Ability1; | |
| gimmi.Ability2 = ghold.Ability2; | |
| gimmi.AbilityH = ghold.AbilityH; | |
| } | |
| private static void CopyPokemonTypesToEvolutions() | |
| { | |
| foreach (var t in PersonalInfoEdited.Table.Where(z => z.IsPresentInGame && z.Info.SpeciesNational != 0133 && z.Evolutions.Count != 0)) | |
| { | |
| // first evo | |
| bool GainNewTypeFirst = GetRandom(100) < GAIN_RANDOM_TYPE_EVO_1_CHANCE_PERCENT; | |
| foreach (var e in t.Evolutions) | |
| { | |
| var evoS1 = e.SpeciesInternal; | |
| var evoF1 = e.Form; | |
| var evo1 = PersonalInfoEdited.Table.FirstOrDefault(z => z.Info.SpeciesInternal == evoS1 && z.Info.Form == evoF1); | |
| evo1.Type1 = t.Type1; | |
| evo1.Type2 = t.Type2; | |
| // gain a second type upon first evolution | |
| if (evo1.Type1 == evo1.Type2 && GainNewTypeFirst) | |
| { | |
| while (evo1.Type2 == evo1.Type1) | |
| evo1.Type2 = GetRandomType(); | |
| } | |
| // second evo | |
| foreach (var f in evo1.Evolutions) | |
| { | |
| bool GainNewTypeSecond = GetRandom(100) < GAIN_RANDOM_TYPE_EVO_2_CHANCE_PERCENT; | |
| var evoS2 = f.SpeciesInternal; | |
| var evoF2 = f.Form; | |
| var evo2 = PersonalInfoEdited.Table.FirstOrDefault(z => z.Info.SpeciesInternal == evoS2 && z.Info.Form == evoF2); | |
| evo2.Type1 = evo1.Type1; | |
| evo2.Type2 = evo1.Type2; | |
| // gain a second type upon second evolution | |
| if (evo2.Type1 == evo2.Type2 && GainNewTypeSecond) | |
| { | |
| while (evo2.Type2 == evo2.Type1) | |
| evo2.Type2 = GetRandomType(); | |
| } | |
| } | |
| } | |
| } | |
| // Gimmighoul edge case | |
| var gimmi = PersonalInfoEdited.Table.FirstOrDefault(z => z.Info.SpeciesNational == 0999 && z.Info.Form == 0); | |
| var ghold = PersonalInfoEdited.Table.FirstOrDefault(z => z.Info.SpeciesNational == 1000); | |
| gimmi.Type1 = ghold.Type1; | |
| gimmi.Type2 = ghold.Type2; | |
| } | |
| private static void GetLearnsetWithTypeBias(IList<PersonalInfoMove> Learnset, ushort species, byte form) | |
| { | |
| var types = GetPokemonTypes((DevID)species, form); | |
| var movesBias = GetRandomMoveListWithBias(species, form, types.Type1, types.Type2); | |
| var movesNoBias = GetRandomMoveList().Except(movesBias).ToList(); | |
| var ctr = 0; | |
| var exhausted = 0; | |
| // to avoid potential form mismatches, do not allow Secret Sword to be a level up move for Keldeo | |
| if (species == 0647) | |
| { | |
| movesBias.Remove(548); | |
| movesNoBias.Remove(548); | |
| } | |
| for (int i = 0; i < Learnset.Count; i++) | |
| { | |
| bool bias = exhausted >= movesBias.Count ? false : GetRandom(100) < RANDOM_LEARNSET_TYPE_BIAS_PERCENT; | |
| Learnset[i].Move = bias ? (ushort)movesBias[exhausted] : (ushort)movesNoBias[i]; | |
| ctr++; | |
| if (bias) | |
| exhausted++; | |
| } | |
| if (GUARANTEE_FOUR_MOVES_AT_LEVEL_ONE && Learnset.Count(z => z.Level == 1) < 4) | |
| { | |
| while (Learnset.Count(z => z.Level == 1) < 4) | |
| { | |
| bool bias = exhausted == movesBias.Count ? false : GetRandom(100) < RANDOM_LEARNSET_TYPE_BIAS_PERCENT; | |
| var move = bias ? (ushort)movesBias[exhausted] : (ushort)movesNoBias[ctr]; | |
| Learnset.Add(new PersonalInfoMove { Move = move, Level = 1 }); | |
| ctr++; | |
| if (bias) | |
| exhausted++; | |
| } | |
| } | |
| } | |
| private static void GetMoveListWithTypeBias(IList<ushort> MoveList, ushort species, byte form) | |
| { | |
| var types = GetPokemonTypes((DevID)species, form); | |
| var movesBias = GetRandomMoveListWithBias(species, form, types.Type1, types.Type2); | |
| var movesNoBias = GetRandomMoveList().Except(movesBias).ToList(); | |
| var ctr = 0; | |
| var exhausted = 0; | |
| for (int i = 0; i < MoveList.Count; i++) | |
| { | |
| // skip Secret Sword, Psyshield Bash, and Dragon Cheer | |
| if (MoveList[i] is 548 or 828 or 913) | |
| continue; | |
| bool bias = exhausted >= movesBias.Count ? false : GetRandom(100) < RANDOM_LEARNSET_TYPE_BIAS_PERCENT; | |
| MoveList[i] = bias ? (ushort)movesBias[exhausted] : (ushort)movesNoBias[i]; | |
| ctr++; | |
| if (bias) | |
| exhausted++; | |
| } | |
| } | |
| private static void AddImportantMovesToReminderMoves(ushort species, ushort form) | |
| { | |
| var pk = PersonalInfoEdited.Table.FirstOrDefault(z => z.Info.SpeciesNational == species && z.Info.Form == form); | |
| var moves = GetImportantSpeciesMoves(pk.Info.SpeciesNational); | |
| foreach (var m in moves) | |
| pk.ReminderMoves.Add((ushort)m); | |
| } | |
| private static void WriteWildChangesToLog(IList<EncountPokeData> table, string location) | |
| { | |
| List<string> Result = []; | |
| SpoilerLog.Add($"== WILD POKÉMON ({location}) =="); | |
| Result.Add("=============================================================================================================================================================================================================================="); | |
| Result.Add("| No. | Pokémon | Lot | Band | Biome 1 | Biome 2 | Biome 3 | Biome 4 | Held Item | Time |"); | |
| Result.Add("=============================================================================================================================================================================================================================="); | |
| var formStringEntries = GetEntryListAHTBCommon(LANGUAGE, "zkn_form"); | |
| var ordered = table.OrderBy(z => GetNationalDexID((ushort)z.DevId)); | |
| foreach (var pk in ordered) | |
| { | |
| var species = GetNationalDexID((ushort)pk.DevId); | |
| var form = GetFormString(formStringEntries, (ushort)pk.DevId, (ushort)pk.Form); | |
| var formDisplay = GetFormStringDisplay(form, species, (byte)pk.Form); | |
| var name = $"{StringsSpecies[(ushort)pk.DevId]}{formDisplay}"; | |
| var band = GetBandDisplayText(pk.Band, pk.BandRate); | |
| var biome1 = $"{pk.LotValue1, -3} ({BiomeNames[(byte)pk.Biome1]})"; | |
| var biome2 = $"{pk.LotValue2, -3} ({BiomeNames[(byte)pk.Biome2]})"; | |
| var biome3 = $"{pk.LotValue3, -3} ({BiomeNames[(byte)pk.Biome3]})"; | |
| var biome4 = $"{pk.LotValue4, -3} ({BiomeNames[(byte)pk.Biome4]})"; | |
| var item = GetBringItemDisplayText((BringItem)pk.Item); | |
| var time = GetTimeTableDisplayText(pk.Time); | |
| var line = $"| {species:0000} | {name, -45} | {pk.LotValue, -3} | {band, -11} | {biome1, -17} | {biome2, -17} | {biome3, -17} | {biome4, -17} | {item, -32} | {time, -28} |"; | |
| Result.Add(line); | |
| } | |
| Result.Add("=============================================================================================================================================================================================================================="); | |
| // all the same times of day, don't list | |
| if (!MAKE_WILD_POKEMON_TIME_OF_DAY_DEPENDENT) | |
| { | |
| for (int i = 0; i < Result.Count; i++) | |
| Result[i] = Result[i][..^31]; | |
| } | |
| foreach (var t in Result) | |
| SpoilerLog.Add(t); | |
| SpoilerLog.Add(""); | |
| } | |
| private static void WritePersonalChangesToLog() | |
| { | |
| SpoilerLog.Add("== POKÉMON STATS =="); | |
| var table = PersonalInfoEdited.Table.OrderBy(z => z.Info.SpeciesNational).Where(z => z.IsPresentInGame && SpeciesList.Contains((DevID)z.Info.SpeciesInternal)); | |
| var formStringEntries = GetEntryListAHTBCommon(LANGUAGE, "zkn_form"); | |
| var lva = GetLevelAbbreviation(LANGUAGE); | |
| bool IsIgnoreEvolutionMethod(EvolutionType method) => method is EvolutionType.LevelUpSummit or EvolutionType.TowerOfDarkness or EvolutionType.TowerOfWaters or EvolutionType.Hisui; | |
| foreach (var t in table) | |
| { | |
| // skip ride forms | |
| if (t.Info.SpeciesNational is 1007 or 1008 && t.Info.Form != 0) | |
| continue; | |
| var name = StringsSpecies[t.Info.SpeciesInternal]; | |
| var form = GetFormString(formStringEntries, t.Info.SpeciesInternal, (byte)t.Info.Form); | |
| var formDisplay = GetFormStringDisplay(form, t.Info.SpeciesNational, (byte)t.Info.Form); | |
| SpoilerLog.Add("====="); | |
| SpoilerLog.Add($"{t.Info.SpeciesNational:0000} {name}{formDisplay}"); | |
| SpoilerLog.Add("====="); | |
| // base stats | |
| var b = t.Base; | |
| var total = b.HP + b.ATK + b.DEF + b.SPA + b.SPD + b.SPE; | |
| var stats = $"{b.HP}.{b.ATK}.{b.DEF}.{b.SPA}.{b.SPD}.{b.SPE}"; | |
| SpoilerLog.Add($"- Base Stats: {stats} (BST: {total:N0})"); | |
| // types | |
| var type = t.Type1 == t.Type2 ? StringsType[t.Type1] : $"{StringsType[t.Type1]}{TextSeparator}{StringsType[t.Type2]}"; | |
| SpoilerLog.Add($"- Type: {type}"); | |
| // abilities | |
| var ability1 = IsAbilityVague(t.Ability1) ? GetDescriptiveAbilityName(t.Ability1, StringsAbility, StringsSpecies, StringsItem) : StringsAbility[t.Ability1]; | |
| var ability2 = IsAbilityVague(t.Ability2) ? GetDescriptiveAbilityName(t.Ability2, StringsAbility, StringsSpecies, StringsItem) : StringsAbility[t.Ability2]; | |
| var abilityH = IsAbilityVague(t.AbilityH) ? GetDescriptiveAbilityName(t.AbilityH, StringsAbility, StringsSpecies, StringsItem) : StringsAbility[t.AbilityH]; | |
| SpoilerLog.Add($"- Abilities: {ability1} (1){TextSeparator}{ability2} (2){TextSeparator}{abilityH} (H)"); | |
| // evolutions | |
| var evos = t.Evolutions; | |
| if (evos.Count != 0) | |
| { | |
| SpoilerLog.Add("- Evolutions:"); | |
| foreach (var e in evos.Where(z => !IsIgnoreEvolutionMethod((EvolutionType)z.Method))) | |
| { | |
| var evoSpecies = StringsSpecies[e.SpeciesInternal]; | |
| var evoForm = GetFormString(formStringEntries, e.SpeciesInternal, (byte)e.Form); | |
| var evoFormDisplay = GetFormStringDisplay(evoForm, GetNationalDexID(e.SpeciesInternal), (byte)e.Form); | |
| var evoIndex = evos.IndexOf(e); | |
| var method = GetEvolutionMethodDisplayText(e, evoIndex); | |
| var level = e.Level == 0 || (e.Level != 0 && e.Method is (ushort)EvolutionType.UseItem or (ushort)EvolutionType.UseItemMale) ? string.Empty : $" ({lva}{e.Level})"; | |
| SpoilerLog.Add($" - {method}: {evoSpecies}{evoFormDisplay}{level}"); | |
| } | |
| } | |
| // learnsets | |
| SpoilerLog.Add("- Level Up Moves:"); | |
| foreach (var m in t.Learnset) | |
| { | |
| var level = m.Level == -3 ? 0 : m.Level; | |
| SpoilerLog.Add($" [{level:000}] {StringsMove[m.Move]}"); | |
| } | |
| if (t.ReminderMoves.Count != 0) | |
| { | |
| SpoilerLog.Add("- Reminder Moves:"); | |
| foreach (var r in t.ReminderMoves) | |
| SpoilerLog.Add($" - {StringsMove[r]}"); | |
| } | |
| if (t.EggMoves.Count != 0) | |
| { | |
| SpoilerLog.Add("- Egg Moves:"); | |
| foreach (var e in t.EggMoves) | |
| SpoilerLog.Add($" - {StringsMove[e]}"); | |
| } | |
| SpoilerLog.Add(""); | |
| } | |
| } | |
| private static string GetEvolutionMethodDisplayText(PersonalInfoEvolution evo, int evoIndex) | |
| { | |
| var template = GetEvolutionMethodFormatted(evo.Method, evoIndex); | |
| var formatted = FormatEvolutionMethodText(evo, template); | |
| return formatted; | |
| } | |
| private static string FormatEvolutionMethodText(PersonalInfoEvolution evo, string template) | |
| { | |
| var type = (EvolutionType)evo.Method; | |
| var source = type switch | |
| { | |
| EvolutionType.TradeHeldItem or EvolutionType.UseItem or EvolutionType.UseItemMale or EvolutionType.UseItemFemale or EvolutionType.LevelUpHeldItemDay or EvolutionType.LevelUpHeldItemNight => StringsItem, | |
| EvolutionType.LevelUpKnowMove or EvolutionType.LevelUpKnowMoveECElse or EvolutionType.LevelUpKnowMoveEC25 => StringsMove, | |
| _ => Array.Empty<string>(), | |
| }; | |
| var arg = source == StringsItem ? GetItemIDFromPlib(evo.Argument) : evo.Argument; | |
| if (source.Length == 0) | |
| return string.Format(template, arg); | |
| return string.Format(template, source[arg]); | |
| } | |
| private static int GetItemIDFromPlib(ushort arg) | |
| { | |
| const string file = "plib_item_conversion_array.bin"; | |
| var path = Path.Combine(DEST_MASTER, "world/data/battle/plib_item_conversion/"); | |
| var obj = FlatBufferConverter.DeserializeFrom<ItemTableArray>(Path.Combine(path, file)); | |
| var entry = obj.Table.Where(z => z.ItemID != 0).FirstOrDefault(z => z.PlibID == arg); | |
| if (entry == null) | |
| return arg; | |
| return entry.ItemID; | |
| } | |
| private static string GetEvolutionMethodFormatted(ushort type, int evoIndex) => (EvolutionType)type switch | |
| { | |
| EvolutionType.LevelUpFriendship => "Level Up (160 Friendship)", | |
| EvolutionType.LevelUpFriendshipMorning => "Level Up (160 Friendship, Day)", | |
| EvolutionType.LevelUpFriendshipNight => "Level Up (160 Friendship, Night)", | |
| EvolutionType.LevelUp => "Level Up", | |
| EvolutionType.Trade => "Trade", | |
| EvolutionType.TradeHeldItem => "Trade (Hold {0})", | |
| EvolutionType.UseItem => "Use {0}", | |
| EvolutionType.LevelUpATK => "Level Up (Attack > Defense)", | |
| EvolutionType.LevelUpAeqD => "Level Up (Attack = Defense)", | |
| EvolutionType.LevelUpDEF => "Level Up (Attack < Defense)", | |
| EvolutionType.LevelUpBeauty => "Level Up ({0} Beauty)", | |
| EvolutionType.UseItemMale => "Use {0} (Male)", | |
| EvolutionType.UseItemFemale => "Use {0} (Female)", | |
| EvolutionType.LevelUpHeldItemDay => "Level Up (Hold {0}, Day)", | |
| EvolutionType.LevelUpHeldItemNight => "Level Up (Hold {0}, Night)", | |
| EvolutionType.LevelUpKnowMove => "Level Up (Know {0})", | |
| EvolutionType.LevelUpMale => "Level Up (Male)", | |
| EvolutionType.LevelUpFemale or EvolutionType.LevelUpFormFemale1 => "Level Up (Female)", | |
| EvolutionType.LevelUpInverted => "Level Up (Upside Down Console)", | |
| EvolutionType.LevelUpAffection50MoveType => "Level Up (160 Friendship, Fairy-type Move)", | |
| EvolutionType.LevelUpWeather => "Level Up (Rain or Fog)", | |
| EvolutionType.LevelUpMorning => "Level Up (Day)", | |
| EvolutionType.LevelUpNight => "Level Up (Night)", | |
| EvolutionType.LevelUpDusk => "Level Up (Evening)", | |
| EvolutionType.LevelUpNatureAmped or EvolutionType.LevelUpNatureLowKey => "Level Up (Certain Nature)", | |
| EvolutionType.LevelUpWalkStepsWith => "Level Up (1,000 Steps)", | |
| EvolutionType.LevelUpUnionCircle => "Level Up (Union Circle)", | |
| EvolutionType.LevelUpInBattleEC25 => "Level Up (In Battle, 1% Chance)", | |
| EvolutionType.LevelUpInBattleECElse => "Level Up (In Battle, 99% Chance)", | |
| EvolutionType.LevelUpCollect999 => "Level Up ({0} Gimmighoul Coins in Bag)", | |
| EvolutionType.LevelUpDefeatEquals => "Level Up (Defeat {0} Bisharp Holding Leader's Crest)", | |
| EvolutionType.LevelUpUseMoveSpecial => "Level Up (Use Rage Fist {0} times)", | |
| EvolutionType.LevelUpKnowMoveECElse => "Level Up (Know {0}, 99% Chance)", | |
| EvolutionType.LevelUpKnowMoveEC25 => "Level Up (Know {0}, 1% Chance)", | |
| EvolutionType.LevelUpRecoilDamageMale => "Level Up ({0} Recoil Damage, Male)", | |
| EvolutionType.LevelUpRecoilDamageFemale => "Level Up ({0} Recoil Damage, Female)", | |
| EvolutionType.Spin when evoIndex == 0 => "Spin (Clockwise, Less Than 5 Seconds, Day)", | |
| EvolutionType.Spin when evoIndex == 1 => "Spin (Counterclockwise, Less Than 5 Seconds, Day)", | |
| EvolutionType.Spin when evoIndex == 2 => "Spin (Clockwise, Less Than 5 Seconds, Night)", | |
| EvolutionType.Spin when evoIndex == 3 => "Spin (Counterclockwise, More Than 5 Seconds, Night)", | |
| EvolutionType.Spin when evoIndex == 4 => "Spin (Clockwise, More Than 5 Seconds, Night)", | |
| EvolutionType.Spin when evoIndex == 5 => "Spin (Counterclockwise, Less Than 5 Seconds, Night)", | |
| EvolutionType.Spin when evoIndex == 6 => "Spin (Counterclockwise, More Than 5 Seconds, Day)", | |
| EvolutionType.Spin when evoIndex == 7 => "Spin (Clockwise, More Than 5 Seconds, Day)", | |
| EvolutionType.Spin when evoIndex == 8 => "Spin (Either Direction, More Than 10 Seconds, Evening)", | |
| _ => throw new ArgumentException(nameof(type)), | |
| }; | |
| private static void WriteFixedSymbolChangesToLog(List<FixedSymbolTable> newEncounters) | |
| { | |
| SpoilerLog.Add("== FIXED SYMBOL ENCOUNTERS =="); | |
| SpoilerLog.Add("============================================================================================================================================================"); | |
| SpoilerLog.Add("| Location | Old Pokémon | New Pokémon | Fixed Tera Type |"); | |
| SpoilerLog.Add("============================================================================================================================================================"); | |
| var formStringEntries = GetEntryListAHTBCommon(LANGUAGE, "zkn_form"); | |
| var oldEncounters = GetOldFixedSymbolEncounters(); | |
| for (int i = 0; i < newEncounters.Count; i++) | |
| { | |
| var encOld = oldEncounters[i].Symbol; | |
| var encNew = newEncounters[i].Symbol; | |
| var key = newEncounters[i].TableKey; | |
| // unused Groudon/Ursaring encounters | |
| if (key is "sample" or "takara_test") | |
| continue; | |
| var formOld = GetFormString(formStringEntries, (ushort)encOld.DevId, (ushort)encOld.FormId); | |
| var formNew = GetFormString(formStringEntries, (ushort)encNew.DevId, (ushort)encNew.FormId); | |
| var formOldDisplay = GetFormStringDisplay(formOld, GetNationalDexID((ushort)encOld.DevId), (byte)encOld.FormId); | |
| var formNewDisplay = GetFormStringDisplay(formNew, GetNationalDexID((ushort)encNew.DevId), (byte)encNew.FormId); | |
| var area = GetLocationNameFromAreaID(key, false); | |
| var nameOld = $"{StringsSpecies[(ushort)encOld.DevId]}{formOldDisplay}"; | |
| var nameNew = $"{StringsSpecies[(ushort)encNew.DevId]}{formNewDisplay}"; | |
| var tera = encNew.GemType is GemType.DEFAULT or GemType.RANDOM ? string.Empty : GetTeraTypeDisplayString(encNew.GemType); | |
| SpoilerLog.Add($"| {area, -28} | {nameOld, -45} | {nameNew, -45} | {tera, -25} |"); | |
| } | |
| SpoilerLog.Add("============================================================================================================================================================"); | |
| SpoilerLog.Add(""); | |
| } | |
| private static void WriteStaticChangesToLog(List<EventBattlePokemon> newEncounters) | |
| { | |
| SpoilerLog.Add("== STATIC ENCOUNTERS =="); | |
| SpoilerLog.Add("================================================================================================================================"); | |
| SpoilerLog.Add("| Location | Old Pokémon | New Pokémon |"); | |
| SpoilerLog.Add("================================================================================================================================"); | |
| bool IsExclusivePair(string key) => key is "1075_multi_a_01" or "1095_multi_a_02" or "1180_multi_a_01" or "1180_multi_a_02" or "1180_multi_a_03" | |
| or "nusi_978_01" or "SDC02_area0_sunanokegawa" or "SDC02_sub_Aentei" or "SDC02_sub_Araikou" or "sub018_AIGUANA"; | |
| bool IsIgnoreLogKey(string key) => key is "1075_multi_b_01" or "1095_multi_b_02" or "1180_multi_b_01" or "1180_multi_b_02" or "1180_multi_b_03" | |
| or "coin_976_05" or "coin_976_10" or "coin_976_15" or "coin_976_20" or "coin_976_25" or "coin_976_30" or "coin_976_35" | |
| or "coin_976_40" or "coin_976_45" or "coin_976_50" or "coin_976_55" or "coin_976_60" or "gym_kusa_020_KIMAWARI_02" | |
| or "gym_kusa_020_KIMAWARI_03" or "gym_kusa_020_KIMAWARI_04" or "gym_kusa_020_KIMAWARI_05" or "lastbattle_AIGUANA" | |
| or "lastbattle_BIGUANA" or "nusi_931_02" or "nusi_944_02" or "nusi_952_dummy_pink" or "nusi_952_dummy_red" or "nusi_952_dummy_yellow" | |
| or "nusi_959_02" or "nusi_962_02" or "nusi_978_02" or "nusi_986_01" or "nusi_986_02" or "SDC01_dokuzaru_01_strong" or "SDC01_get_dokuzaru" | |
| or "SDC01_kamenoni_1" or "SDC01_kamenoni_1_strong" or "SDC01_kamenoni_2" or "SDC01_kamenoni_2_strong" or "SDC01_kamenoni_3" | |
| or "SDC01_kamenoni_3_strong" or "SDC01_kamenoni_4_strong" or "SDC01_midoriikenushi_strong" or "SDC01_onitaizi_bird" or "SDC01_onitaizi_bird_strong" | |
| or "SDC01_onitaizi_dokuinu" or "SDC01_onitaizi_dokuinu_strong" or "SDC01_onitaizi_monkey" or "SDC01_onitaizi_monkey_strong" or "SDC02_0310_kodaikame" | |
| or "SDC02_area0_tetunoibara" or "SDC02_sub_Bkobaruon" or "SDC02_sub_Bterakion" or "sub018_BIGUANA"; | |
| bool IsIgnoreFormString(string key) => key is "nusi_952_01" or "SDC01_kamenoni_4" or "SDC02_0330_kodaikame"; | |
| ushort GetOppositeDevID(string key) => key switch | |
| { | |
| "1075_multi_a_01" => 0992, // Iron Bundle | |
| "1095_multi_a_02" or "1180_multi_a_01" or "nusi_978_01" => 0986, // Iron Treads | |
| "1180_multi_a_02" => 0989, // Iron Hands | |
| "1180_multi_a_03" => 0990, // Iron Jugulis | |
| "SDC02_area0_sunanokegawa" => 0991, // Iron Thorns | |
| "SDC02_sub_Aentei" => 1020, // Iron Boulder | |
| "SDC02_sub_Araikou" => 1019, // Iron Crown | |
| "sub018_AIGUANA" => 0999, // Miraidon | |
| _ => throw new ArgumentException(nameof(key), key), | |
| }; | |
| var locationIndices = new[] | |
| { | |
| 110, 110, 110, 110, 110, 110, 000, | |
| 070, 064, 086, 040, 032, 040, 022, | |
| 020, 024, 109, 022, 006, 048, 156, | |
| 162, 144, 146, 152, 166, 166, 110, | |
| 110, 196, 196, 196, 196, 196, 138, | |
| 196, 048, 048, 032, 038, 038, 067, | |
| 014, 040, 069, 014, 048, 062, 040, | |
| 024, 109, 022, 050, 034, 010, 038, | |
| 040, 022, 030, 006, 018, 176, 110, | |
| }; | |
| var formStringEntries = GetEntryListAHTBCommon(LANGUAGE, "zkn_form"); | |
| var oldEncounters = GetOldStaticEncounters(); | |
| var ctr = 0; | |
| for (int i = 0; i < newEncounters.Count; i++) | |
| { | |
| var encOld = oldEncounters[i].PokeData; | |
| var encNew = newEncounters[i].PokeData; | |
| var key = newEncounters[i].Label; | |
| // only list one of each instance of an encounter | |
| if (IsIgnoreLogKey(key)) | |
| continue; | |
| var formOld = GetFormString(formStringEntries, (ushort)encOld.DevId, (ushort)encOld.FormId); | |
| var formNew = GetFormString(formStringEntries, (ushort)encNew.DevId, (ushort)encNew.FormId); | |
| var formOldDisplay = GetFormStringDisplay(formOld, GetNationalDexID((ushort)encOld.DevId), (byte)encOld.FormId); | |
| var formNewDisplay = GetFormStringDisplay(formNew, GetNationalDexID((ushort)encNew.DevId), (byte)encNew.FormId); | |
| var pair = string.Empty; | |
| // pair version exclusives together | |
| if (IsExclusivePair(key)) | |
| { | |
| var opposite = GetOppositeDevID(key); | |
| pair = $"{TextSeparator}{StringsSpecies[opposite]}"; | |
| formOldDisplay = string.Empty; | |
| } | |
| // multiple instances of an encounter, but you encounter it in multiple forms, so be vague | |
| if (IsIgnoreFormString(key)) | |
| formOldDisplay = string.Empty; | |
| var nameOld = $"{StringsSpecies[(ushort)encOld.DevId]}{formOldDisplay}{pair}"; | |
| var nameNew = $"{StringsSpecies[(ushort)encNew.DevId]}{formNewDisplay}"; | |
| var area = locationIndices[ctr] == 0 ? string.Empty : StringsLocation[locationIndices[ctr]]; | |
| SpoilerLog.Add($"| {area, -28} | {nameOld, -45} | {nameNew, -45} |"); | |
| ctr++; | |
| } | |
| SpoilerLog.Add("================================================================================================================================"); | |
| SpoilerLog.Add(""); | |
| } | |
| private static void WriteGiftChangesToLog(List<EventAddPokemon> newEncounters) | |
| { | |
| SpoilerLog.Add("== GIFT ENCOUNTERS =="); | |
| SpoilerLog.Add("================================================================================================="); | |
| SpoilerLog.Add("| Old Pokémon | New Pokémon |"); | |
| SpoilerLog.Add("================================================================================================="); | |
| var formStringEntries = GetEntryListAHTBCommon(LANGUAGE, "zkn_form"); | |
| var oldEncounters = GetOldGiftEncounters(); | |
| for (int i = 0; i < newEncounters.Count; i++) | |
| { | |
| var encOld = oldEncounters[i].PokeData; | |
| var encNew = newEncounters[i].PokeData; | |
| var key = newEncounters[i].Label; | |
| // handle Koraidon/Miraidon separately | |
| if (key == "gameclear_BIGUANA") | |
| continue; | |
| var formOld = GetFormString(formStringEntries, (ushort)encOld.DevId, (ushort)encOld.FormId); | |
| var formNew = GetFormString(formStringEntries, (ushort)encNew.DevId, (ushort)encNew.FormId); | |
| var formOldDisplay = GetFormStringDisplay(formOld, GetNationalDexID((ushort)encOld.DevId), (byte)encOld.FormId); | |
| var formNewDisplay = GetFormStringDisplay(formNew, GetNationalDexID((ushort)encNew.DevId), (byte)encNew.FormId); | |
| var pair = string.Empty; | |
| // pair Koraidon/Miraidon together | |
| if (key == "gameclear_AIGUANA") | |
| { | |
| pair = $"{TextSeparator}{StringsSpecies[0999]}"; | |
| formOldDisplay = string.Empty; | |
| } | |
| var nameOld = $"{StringsSpecies[(ushort)encOld.DevId]}{formOldDisplay}{pair}"; | |
| var nameNew = $"{StringsSpecies[(ushort)encNew.DevId]}{formNewDisplay}"; | |
| SpoilerLog.Add($"| {nameOld, -45} | {nameNew, -45} |"); | |
| } | |
| SpoilerLog.Add("================================================================================================="); | |
| SpoilerLog.Add(""); | |
| } | |
| private static void WriteTrainerChangesToLog(IList<TrDataMain> table) | |
| { | |
| SpoilerLog.Add("== TRAINER BATTLES =="); | |
| const string file = "trtype_array.bin"; | |
| var path = Path.Combine(PATH_MASTER, "world/data/trainer/trtype/"); | |
| var tta = FlatBufferConverter.DeserializeFrom<TrainerTypeArray>(Path.Combine(path, file)); | |
| var trainerNameEntries = GetEntryListAHTBCommon(LANGUAGE, "trname"); | |
| var trainerClassEntries = GetEntryListAHTBCommon(LANGUAGE, "trtype"); | |
| bool IsNemona(string id) => id.Contains("rival_") || id.Contains("nemo_"); | |
| bool IsClavell(string id) => id.Contains("clavel_") || id.Contains("claver_"); | |
| var formStringEntries = GetEntryListAHTBCommon(LANGUAGE, "zkn_form"); | |
| var lva = GetLevelAbbreviation(LANGUAGE); | |
| var trClassFlags = GetStringFlagsCommon(LANGUAGE, "trtype"); | |
| SpoilerLog.Add("===================================================================================================================================================================================================================="); | |
| SpoilerLog.Add("| Trainer | Pokémon 1 | Pokémon 2 | Pokémon 3 | Pokémon 4 | Pokémon 5 | Pokémon 6 |"); | |
| SpoilerLog.Add("===================================================================================================================================================================================================================="); | |
| foreach (var t in table) | |
| { | |
| var type = GetTrainerClass(t.TrainerType); | |
| var name = GetTrainerName(t.TrNameLabel); | |
| var index = StringsTrainerType.IndexOf(type); | |
| var flags = index == -1 ? trClassFlags[0] : trClassFlags[index]; | |
| var display = LANGUAGE == "English" ? GetTrainerNameDisplay(type, name, flags) : $"{type} {name}".Trim(); | |
| // unused | |
| if ((string.IsNullOrWhiteSpace(type) && string.IsNullOrWhiteSpace(name)) || name.Contains("[~ ")) | |
| continue; | |
| if (display.Contains("INVALID") && t.TrId is not ("professor_A_01" or "professor_A_02" or "professor_B_01" or "professor_B_02")) | |
| continue; | |
| if (!t.TrId.Contains("kusa") && !t.TrId.Contains("hono") && !t.TrId.Contains("mizu") && (IsNemona(t.TrId) || IsClavell(t.TrId))) | |
| continue; | |
| var team = new[] { t.Poke1, t.Poke2, t.Poke3, t.Poke4, t.Poke5, t.Poke6 }; | |
| List<string> Members = []; | |
| foreach (var pk in team) | |
| { | |
| var form = pk.FormId == 0 ? string.Empty : $"-{pk.FormId}"; | |
| var line = pk.DevId == DevID.DEV_NULL ? " " : $" {StringsSpecies[(ushort)pk.DevId]}{form} ({lva}{pk.Level})"; | |
| Members.Add($"{line, -27}"); | |
| } | |
| var trNameDisplay = display.Replace("INVALID", " ").Trim(); | |
| SpoilerLog.Add($"| {trNameDisplay, -40} |{string.Join('|', Members), -165}|"); | |
| } | |
| SpoilerLog.Add("===================================================================================================================================================================================================================="); | |
| SpoilerLog.Add(""); | |
| string GetTrainerName(string name) | |
| { | |
| var tr = trainerNameEntries.FirstOrDefault(z => z.Name == name); | |
| if (tr == null) | |
| return "INVALID"; | |
| var index = trainerNameEntries.IndexOf(tr); | |
| return StringsTrainerName[index]; | |
| } | |
| string GetTrainerClass(string type) | |
| { | |
| var tr = tta.Table.FirstOrDefault(z => z.NameLabel == type); | |
| if (tr == null) | |
| return "INVALID"; | |
| var ty = trainerClassEntries.FirstOrDefault(z => z.Name == tr.MsgLabel); | |
| if (ty == null) | |
| return "INVALID"; | |
| var index = trainerClassEntries.IndexOf(ty); | |
| return StringsTrainerType[index]; | |
| } | |
| string GetTrainerNameDisplay(string type, string name, ushort flags) | |
| { | |
| bool article = ((flags >> 9) & 1) == 0; | |
| var format = ((flags >> 7) & 0b11); | |
| var display = format switch | |
| { | |
| 0 => "{0} {1}", | |
| 1 when article => "{1} of the {0}", | |
| 1 => "{1} of {0}", | |
| 2 => "{1} the {0}", | |
| _ => throw new ArgumentOutOfRangeException(nameof(format), format, null), | |
| }; | |
| if (format == 0 && name is "Giacomo" or "Mela" or "Atticus" or "Ortega" or "Eri" or "Penny" && type == "Team Star") | |
| display = "{1} of {0}"; | |
| return string.Format(display, type, name); | |
| } | |
| } | |
| private static void WriteFieldItemChangesToLog(TrinitySceneObjectTemplate obj, List<(ulong Item, ulong Quantity)> OriginalFieldItems, List<string> ItemNames, int region) | |
| { | |
| var location = GetRegionName(region).ToUpper(); | |
| SpoilerLog.Add($"== FIELD ITEMS ({location}) =="); | |
| List<string> Result = []; | |
| var ctr = 0; | |
| Result.Add("========================================================================================================================"); | |
| Result.Add("| Location | Old Item | New Item |"); | |
| Result.Add("========================================================================================================================"); | |
| foreach (var o in obj.Objects) | |
| { | |
| foreach (var s in o.SubObjects.Where(z => z.Type == "trinity_PropertySheet")) | |
| { | |
| var tps = FlatBufferConverter.DeserializeFrom<TrinityPropertySheet>(s.Data); | |
| foreach (var p in tps.Properties) | |
| { | |
| foreach (var i in p.Fields.Where(z => z.Name == "itemInfo")) | |
| { | |
| var item = i.Data.Item9.Fields.FirstOrDefault(z => z.Name == "itemNo").Data.TrinityPropertySheetField1.Value; | |
| var quantity = i.Data.Item9.Fields.FirstOrDefault(z => z.Name == "num").Data.TrinityPropertySheetField1.Value; | |
| var area = ItemNames[ctr]; | |
| var itemDisplayOld = $"{StringsItem[OriginalFieldItems[ctr].Item]} (×{OriginalFieldItems[ctr].Quantity})"; | |
| var itemDisplayNew = $"{StringsItem[item]} (×{quantity})"; | |
| if (GetIsTechnicalMachine(item)) | |
| { | |
| itemDisplayOld = GetTechnicalMachineDisplay(OriginalFieldItems[ctr].Item, true); | |
| itemDisplayNew = GetTechnicalMachineDisplay(item, false); | |
| } | |
| Result.Add($"| {GetLocationNameFromAreaID(area, true), -28} | {itemDisplayOld, -41} | {itemDisplayNew, -41} |"); | |
| ctr++; | |
| } | |
| } | |
| } | |
| } | |
| Result.Add("========================================================================================================================"); | |
| Result.Add(""); | |
| // sub-zones, only list items | |
| if (region is >= 3 and <= 6) | |
| { | |
| for (int i = 0; i < Result.Count - 1; i++) | |
| Result[i] = Result[i][31..]; | |
| } | |
| foreach (var t in Result) | |
| SpoilerLog.Add(t); | |
| } | |
| private static void WriteTeraRaidChangesToLog(List<(DevID Species, short Form, ItemID Item, int Stars)> oldEncounters, List<(DevID Species, short Form, ItemID Item, int Stars)> newEncounters, int area) | |
| { | |
| var formStringEntries = GetEntryListAHTBCommon(LANGUAGE, "zkn_form"); | |
| var location = GetRegionName(area).ToUpper(); | |
| SpoilerLog.Add($"== TERA RAID BATTLE ENCOUNTERS ({location}) =="); | |
| SpoilerLog.Add("==============================================================================================================="); | |
| SpoilerLog.Add("| Difficulty | Old Pokémon | New Pokémon |"); | |
| SpoilerLog.Add("==============================================================================================================="); | |
| for (int i = 0; i < newEncounters.Count; i++) | |
| { | |
| var encOld = oldEncounters[i]; | |
| var encNew = newEncounters[i]; | |
| var stars = newEncounters[i].Stars; | |
| var formOld = GetFormString(formStringEntries, (ushort)encOld.Species, (ushort)encOld.Form); | |
| var formNew = GetFormString(formStringEntries, (ushort)encNew.Species, (ushort)encNew.Form); | |
| var formOldDisplay = GetFormStringDisplay(formOld, GetNationalDexID((ushort)encOld.Species), (byte)encOld.Form); | |
| var formNewDisplay = GetFormStringDisplay(formNew, GetNationalDexID((ushort)encNew.Species), (byte)encNew.Form); | |
| var starsDisplay = $"{stars}-Star"; | |
| var nameOld = $"{StringsSpecies[(ushort)encOld.Species]}{formOldDisplay}"; | |
| var nameNew = $"{StringsSpecies[(ushort)encNew.Species]}{formNewDisplay}"; | |
| SpoilerLog.Add($"| {starsDisplay, -11} | {nameOld, -45} | {nameNew, -45} |"); | |
| } | |
| SpoilerLog.Add("==============================================================================================================="); | |
| SpoilerLog.Add(""); | |
| } | |
| private static void WriteTechnicalMachineChangesToLog(IList<ShopWazamachineData> table) | |
| { | |
| var lpAbbreviation = GetLPAbbreviation(LANGUAGE); | |
| var tmAbbreviation = StringsItem[0328][..^3]; | |
| var ctr = 1; | |
| SpoilerLog.Add("== TECHNICAL MACHINES =="); | |
| SpoilerLog.Add("==============================================================================================================================================================="); | |
| SpoilerLog.Add($"| Technical Machine | {lpAbbreviation} | Item 1 | Item 2 | Item 3 |"); | |
| SpoilerLog.Add("==============================================================================================================================================================="); | |
| foreach (var t in table.OrderBy(z => z.WazaItemID)) | |
| { | |
| var name = $"{tmAbbreviation}{ctr:000} {StringsMove[t.WazaNo]}"; | |
| var lp = $"{t.LP:N0}"; | |
| var item1 = $"{StringsItem[(ushort)t.Item01]} (×{t.ItemNum01})"; | |
| var item2 = t.Item02 == ItemID.ITEMID_NONE ? string.Empty : $"{StringsItem[(ushort)t.Item02]} (×{t.ItemNum02})"; | |
| var item3 = t.Item03 == ItemID.ITEMID_NONE ? string.Empty : $"{StringsItem[(ushort)t.Item03]} (×{t.ItemNum03})"; | |
| SpoilerLog.Add($"| {name, -40} | {lp, -7} | {item1, -32} | {item2, -32} | {item3, -32} |"); | |
| ctr++; | |
| } | |
| SpoilerLog.Add("==============================================================================================================================================================="); | |
| SpoilerLog.Add(""); | |
| } | |
| private static void WriteTradeChangesToLog(IList<EventTradePokemon> newEncounters, IList<EventTradeList> requestedSpecies) | |
| { | |
| SpoilerLog.Add("== IN-GAME TRADES =="); | |
| SpoilerLog.Add("================================================================================================================================================================================================"); | |
| SpoilerLog.Add("| Location | Trainer | Old Pokémon | New Pokémon | Requested Pokémon |"); | |
| SpoilerLog.Add("================================================================================================================================================================================================"); | |
| var formStringEntries = GetEntryListAHTBCommon(LANGUAGE, "zkn_form"); | |
| var oldEncounters = GetOldTradeEncounters(); | |
| for (int i = 0; i < newEncounters.Count; i++) | |
| { | |
| var reqIndex = i == 32 ? 2 : i is 0 or 1 ? i : 0; | |
| var encOld = oldEncounters[i].PokeData; | |
| var encNew = newEncounters[i].PokeData; | |
| var encReq = requestedSpecies[reqIndex]; | |
| var key = newEncounters[i].Label; | |
| var trainer = GetStringFromAHTBScript(LANGUAGE, "field_trade", encNew.ParentNameLabel); | |
| bool blueberry = key.Contains("trade_gest"); | |
| var formOld = GetFormString(formStringEntries, (ushort)encOld.DevId, (ushort)encOld.FormId); | |
| var formNew = GetFormString(formStringEntries, (ushort)encNew.DevId, (ushort)encNew.FormId); | |
| var formOldDisplay = GetFormStringDisplay(formOld, GetNationalDexID((ushort)encOld.DevId), (byte)encOld.FormId); | |
| var formNewDisplay = GetFormStringDisplay(formNew, GetNationalDexID((ushort)encNew.DevId), (byte)encNew.FormId); | |
| var area = key switch | |
| { | |
| "trade_c02_0194" => StringsLocation[076], // Cascarrafa | |
| "trade_c03_0093" => StringsLocation[078], // Levincia | |
| "trade_t03_0872" => StringsLocation[084], // Cortondo | |
| _ => StringsLocation[172], // Blueberry Academy | |
| }; | |
| var nameOld = $"{StringsSpecies[(ushort)encOld.DevId]}{formOldDisplay}"; | |
| var nameNew = $"{StringsSpecies[(ushort)encNew.DevId]}{formNewDisplay}"; | |
| var nameReq = blueberry ? string.Empty : StringsSpecies[(ushort)encReq.SendPokeDevId]; | |
| SpoilerLog.Add($"| {area, -28} | {trainer, -13} | {nameOld, -45} | {nameNew, -45} | {nameReq, -45} |"); | |
| } | |
| SpoilerLog.Add("================================================================================================================================================================================================"); | |
| SpoilerLog.Add(""); | |
| } | |
| private static void WriteStarBarrageChangesToLog(IList<AjitoPokemon> newEncounters) | |
| { | |
| SpoilerLog.Add("== STAR BARRAGES =="); | |
| SpoilerLog.Add("================================================================================================================================"); | |
| SpoilerLog.Add("| Location | Old Pokémon | New Pokémon |"); | |
| SpoilerLog.Add("================================================================================================================================"); | |
| var formStringEntries = GetEntryListAHTBCommon(LANGUAGE, "zkn_form"); | |
| var oldEncounters = GetOldStarBarrageEncounters(); | |
| string GetAreaName(int index) => index switch | |
| { | |
| >= 001 and <= 100 => StringsLocation[105], // Schedar Squad's Base | |
| >= 101 and <= 200 => StringsLocation[101], // Segin Squad's Base | |
| >= 201 and <= 300 => StringsLocation[107], // Navi Squad's Base | |
| >= 301 and <= 400 => StringsLocation[099], // Caph Squad's Base | |
| >= 401 and <= 500 => StringsLocation[103], // Ruchbah Squad's Base | |
| _ => string.Empty, // Cyclizar is in all bases | |
| }; | |
| for (int i = 0; i < newEncounters.Count; i++) | |
| { | |
| var encOld = oldEncounters[i].Table; | |
| var encNew = newEncounters[i].Table; | |
| var area = GetAreaName(newEncounters[i].Table.PokemonId); | |
| var formOld = GetFormString(formStringEntries, (ushort)encOld.DevId, (ushort)encOld.FormNo); | |
| var formNew = GetFormString(formStringEntries, (ushort)encNew.DevId, (ushort)encNew.FormNo); | |
| var formOldDisplay = GetFormStringDisplay(formOld, GetNationalDexID((ushort)encOld.DevId), (byte)encOld.FormNo); | |
| var formNewDisplay = GetFormStringDisplay(formNew, GetNationalDexID((ushort)encNew.DevId), (byte)encNew.FormNo); | |
| var nameOld = $"{StringsSpecies[(ushort)encOld.DevId]}{formOldDisplay}"; | |
| var nameNew = $"{StringsSpecies[(ushort)encNew.DevId]}{formNewDisplay}"; | |
| SpoilerLog.Add($"| {area, -28} | {nameOld, -45} | {nameNew, -45} |"); | |
| } | |
| SpoilerLog.Add("================================================================================================================================"); | |
| SpoilerLog.Add(""); | |
| } | |
| private static string GetLocationNameFromAreaID(string id, bool forceAreaZero) | |
| { | |
| var index = id.IndexOf("area"); | |
| if (index == -1) | |
| return GetAreaName(id, forceAreaZero); | |
| var areaID = id[index..][..6]; | |
| if (id.Contains("su1")) areaID = "su1_" + areaID; | |
| if (id.Contains("su2")) areaID = "su2_" + areaID; | |
| return GetAreaName(areaID, forceAreaZero); | |
| } | |
| private static string GetAreaName(string id, bool forceAreaZero) => id switch | |
| { | |
| "area01" => StringsLocation[006], // South Province (Area One) | |
| "area02" => StringsLocation[008], // Mesagoza | |
| "area03" => StringsLocation[010], // Pokémon League | |
| "area04" => StringsLocation[012], // South Province (Area Two) | |
| "area05" => StringsLocation[014], // South Province (Area Four) | |
| "area06" => StringsLocation[016], // South Province (Area Six) | |
| "area07" => StringsLocation[018], // South Province (Area Five) | |
| "area08" => StringsLocation[020], // South Province (Area Three) | |
| "area09" => StringsLocation[022], // West Province (Area One) | |
| "area10" => StringsLocation[024], // Asado Desert | |
| "area11" => StringsLocation[026], // West Province (Area Two) | |
| "area12" => StringsLocation[028], // West Province (Area Three) | |
| "area13" => StringsLocation[030], // Tagtree Thicket | |
| "area14" => StringsLocation[032], // East Province (Area Three) | |
| "area15" => StringsLocation[034], // East Province (Area One) | |
| "area16" or "area16_1" => StringsLocation[036], // East Province (Area Two) | |
| "area17" => StringsLocation[038], // Glaseado Mountain (1) | |
| "area18" => StringsLocation[040], // Casseroya Lake | |
| "area19" => StringsLocation[042], // Glaseado Mountain (2) | |
| "area20" => StringsLocation[044], // North Province (Area Three) | |
| "area21" => StringsLocation[046], // North Province (Area One) | |
| "area22" => StringsLocation[048], // North Province (Area Two) | |
| "area23" when !forceAreaZero => StringsLocation[050], // Great Crater of Paldea | |
| "area23" when forceAreaZero => StringsLocation[110], // Area Zero | |
| "area24" => StringsLocation[056], // South Paldean Sea | |
| "area25" => StringsLocation[058], // West Paldean Sea | |
| "area26" => StringsLocation[060], // East Paldean Sea | |
| "area27" => StringsLocation[062], // North Paldean Sea | |
| "su1_area01" => StringsLocation[132], // Kitakami Road | |
| "su1_area02" => StringsLocation[136], // Apple Hills | |
| "su1_area03" => StringsLocation[140], // Reveler's Road | |
| "su1_area04" => StringsLocation[144], // Oni Mountain (1) | |
| "su1_area05" => StringsLocation[147], // Oni Mountain (2) | |
| "su1_area06" => StringsLocation[152], // Crystal Pool | |
| "su1_area07" => StringsLocation[153], // Oni Mountain Summit | |
| "su1_area08" => StringsLocation[156], // Wistful Fields | |
| "su1_area09" => StringsLocation[158], // Mossfell Confluence | |
| "su1_area10" => StringsLocation[160], // Fellhorn Gorge | |
| "su1_area11" => StringsLocation[162], // Paradise Barrens | |
| "su1_area12" => StringsLocation[164], // Kitakami Wilds | |
| "su2_area01" => StringsLocation[174], // Savanna Biome | |
| "su2_area02" => StringsLocation[176], // Coastal Biome | |
| "su2_area03" => StringsLocation[178], // Canyon Biome | |
| "su2_area04" => StringsLocation[180], // Polar Biome | |
| _ when id.Contains("event150") => StringsLocation[064], // Inlet Grotto | |
| _ when id.Contains("w23_i21") => StringsLocation[118], // Zero Lab | |
| _ when id.Contains("city_event") => StringsLocation[182], // Central Plaza | |
| _ when id.Contains("w23_d10") || id.Contains("w23d10") => StringsLocation[196], // Area Zero Underdepths | |
| _ => throw new ArgumentException(nameof(id), id), | |
| }; | |
| private static string GetRegionName(int index) => index switch | |
| { | |
| 0 => StringsMapMain[061], // The Paldea Region | |
| 1 => StringsMapMain[062], // The Land of Kitakami | |
| 2 => StringsMapMain[063], // Blueberry Academy | |
| 3 => StringsLocation[110], // Area Zero | |
| 4 => StringsLocation[008], // Mesagoza | |
| 5 => StringsLocation[118], // Zero Lab | |
| 6 => StringsLocation[196], // Area Zero Underdepths | |
| _ => throw new ArgumentException(nameof(index)), | |
| }; | |
| private static void UpdateMoveProperties() | |
| { | |
| if (UPDATE_MOVE_PROPERTIES_TO_FUTURE_GAME == MovePropertyUpdater.DoNotUpdate) | |
| return; | |
| if (UPDATE_MOVE_PROPERTIES_TO_FUTURE_GAME == MovePropertyUpdater.Champions) | |
| UpdateMovePropertiesToChampions(); | |
| UpdateMoveTable(); | |
| } | |
| private static void UpdateMovePropertiesToChampions() | |
| { | |
| foreach (var t in MoveInfoEdited.Table) | |
| { | |
| var move = t.MoveID; | |
| /* Growth */ if (move == 074) t.Type = (byte)TypeIndex.Grass; | |
| /* Crabhammer */ if (move == 152) t.Accuracy = 95; | |
| /* Bone Rush */ if (move == 198) t.Power = 30; | |
| /* Metal Claw */ if (move == 232) t.FlagSlicing = true; | |
| /* Crush Claw */ if (move == 306) t.FlagSlicing = true; | |
| /* Dragon Claw */ if (move == 337) t.FlagSlicing = true; | |
| /* Shadow Claw */ if (move == 421) t.FlagSlicing = true; | |
| /* Night Daze */ if (move == 539) t.Power = 90; | |
| /* Gear Grind */ if (move == 544) { t.Power = 60; t.Accuracy = 90; } | |
| /* First Impression */ if (move == 660) t.Power = 100; | |
| /* Spirit Shackle */ if (move == 662) t.Power = 90; | |
| /* Anchor Shot */ if (move == 677) t.Power = 90; | |
| /* Fire Lash */ if (move == 680) t.Power = 90; | |
| /* Revelation Dance */ if (move == 686) t.Power = 100; | |
| /* Trop Kick */ if (move == 688) t.Power = 85; | |
| /* Beak Blast */ if (move == 690) t.Power = 120; | |
| /* Dragon Hammer */ if (move == 692) t.Power = 100; | |
| /* Snipe Shot */ if (move == 745) t.Power = 85; | |
| /* Bolt Beak */ if (move == 754) t.Power = 80; | |
| /* Fishious Rend */ if (move == 755) t.Power = 80; | |
| /* Snap Trap */ if (move == 779) t.Type = (byte)TypeIndex.Steel; | |
| /* Apple Acid */ if (move == 787) t.Power = 90; | |
| /* Grav Apple */ if (move == 788) t.Power = 90; | |
| /* Astral Barrage */ if (move == 825) t.Power = 110; | |
| /* Dire Claw */ if (move == 827) t.FlagSlicing = true; | |
| /* Psyshield Bash */ if (move == 828) t.Power = 90; | |
| /* Mountain Gale */ if (move == 836) t.Power = 120; | |
| /* Infernal Parade */ if (move == 844) t.Power = 65; | |
| /* Triple Dive */ if (move == 865) t.Power = 35; | |
| /* Make It Rain */ if (move == 874) t.Accuracy = 95; | |
| /* Hyper Drill */ if (move == 887) t.Power = 120; | |
| /* Blood Moon */ if (move == 901) t.Power = 130; | |
| /* Syrup Bomb */ if (move == 903) t.Accuracy = 90; | |
| /* Dragon Cheer */ if (move == 913) t.FlagSound = true; | |
| if (UPDATE_MOVE_SECONDARY_EFFECTS) | |
| { | |
| // Iron Head: 30% -> 20% | |
| if (move == 442) | |
| t.Flinch = 20; | |
| // Freeze-Dry: 10% -> 0% | |
| if (move == 573) | |
| t.Inflict = new WazaInflict(); | |
| // Moonblast: 30% -> 10% | |
| if (move == 585) | |
| { | |
| var oldAmp = t.StatAmps; | |
| var newAmp = new WazaEntityStat() { Stat1 = oldAmp.Stat1, Stat1Stage = oldAmp.Stat1Stage, Stat1Percent = 10 }; | |
| t.StatAmps = newAmp; | |
| } | |
| // Toxic Thread: -1 Speed -> -2 Speed | |
| if (move == 672) | |
| { | |
| var oldAmp = t.StatAmps; | |
| var newAmp = new WazaEntityStat() { Stat1 = oldAmp.Stat1, Stat1Stage = -2, Stat1Percent = oldAmp.Stat1Percent }; | |
| t.StatAmps = newAmp; | |
| } | |
| // Dire Claw: 50% -> 30% | |
| if (move == 827) | |
| { | |
| var oldInflict = t.Inflict; | |
| var newInflict = new WazaInflict() { Value = oldInflict.Value, Chance = 30, Turn1 = oldInflict.Turn1, Turn2 = oldInflict.Turn2, Turn3 = oldInflict.Turn3 }; | |
| t.Inflict = newInflict; | |
| } | |
| // Make It Rain: -1 Sp. Attack -> -2 Sp. Attack | |
| if (move == 874) | |
| { | |
| var oldAmp = t.StatAmps; | |
| var newAmp = new WazaEntityStat() { Stat1 = oldAmp.Stat1, Stat1Stage = -2, Stat1Percent = oldAmp.Stat1Percent }; | |
| t.StatAmps = newAmp; | |
| } | |
| } | |
| if (UPDATE_MOVE_PP) | |
| { | |
| /* Protect */ if (move == 182) t.PP = 8; | |
| /* Sandstorm */ else if (move == 201) t.PP = 8; | |
| /* Night Slash */ else if (move == 400) t.PP = 20; | |
| /* King's Shield */ else if (move == 588) t.PP = 8; | |
| /* Spiky Shield */ else if (move == 596) t.PP = 8; | |
| /* Baneful Bunker */ else if (move == 661) t.PP = 8; | |
| /* Purify */ else if (move == 685) t.PP = 8; | |
| /* Beak Blast */ else if (move == 690) t.PP = 8; | |
| /* Shell Trap */ else if (move == 704) t.PP = 12; | |
| /* Obstruct */ else if (move == 792) t.PP = 8; | |
| /* Spin Out */ else if (move == 859) t.PP = 12; | |
| /* Snowscape */ else if (move == 883) t.PP = 8; | |
| else | |
| t.PP = CalculateChampionsMovePP(t.PP); | |
| } | |
| } | |
| } | |
| private static void RandomizeMoveProperties() | |
| { | |
| var pp = UPDATE_MOVE_PROPERTIES_TO_FUTURE_GAME == MovePropertyUpdater.Champions ? new[] { 8, 12, 16, 20 } | |
| : new[] { 5, 10, 15, 20, 25, 30, 35, 40 }; | |
| bool IsStatusMove(byte category) => category == 0; | |
| var localePP = GetStringFromAHTBCommon(LANGUAGE, "status", FnvHash.HashFnv1a_64("msg_ui_status_skill_01")); | |
| var localeCategory = GetStringFromAHTBCommon(LANGUAGE, "status", FnvHash.HashFnv1a_64("msg_ui_status_skill_category_00")); | |
| var localePower = GetStringFromAHTBCommon(LANGUAGE, "status", FnvHash.HashFnv1a_64("msg_ui_status_skill_power_00")); | |
| var localeAccuracy = GetStringFromAHTBCommon(LANGUAGE, "status", FnvHash.HashFnv1a_64("msg_ui_status_skill_hit_00")); | |
| var localeType = GetStringFromAHTBCommon(LANGUAGE, "status", FnvHash.HashFnv1a_64("msg_ui_status_pokemon_type_00")); | |
| string[][] categories = | |
| [ | |
| [ "Status", "Physical", "Special" ], | |
| [ "Statut", "Physique", "Spéciale" ], | |
| [ "Status", "Physische", "Spezial" ], | |
| [ "Stato", "Fisica", "Speciale" ], | |
| [ "変化", "物理", "特殊" ], | |
| [ "変化", "物理", "特殊" ], | |
| [ "변화", "물리", "특수" ], | |
| [ "變化", "物理", "特殊" ], | |
| [ "Estado", "Físico", "Especial" ], | |
| [ "變化", "物理", "特殊" ], | |
| ]; | |
| var names = new[] { "Move", "Attaque", "Attacke", "Mossa", "わざ", "わざ", "기술", "招式", "Movimiento", "招式" }; | |
| var localeMove = names[Languages.IndexOf(LANGUAGE)]; | |
| if (RANDOMIZE_MOVE_PROPERTIES_ACCURACY || RANDOMIZE_MOVE_PROPERTIES_CATEGORY || RANDOMIZE_MOVE_PROPERTIES_POWER || RANDOMIZE_MOVE_PROPERTIES_PP || RANDOMIZE_MOVE_PROPERTIES_TYPE) | |
| { | |
| SpoilerLog.Add("== MOVE PROPERTIES =="); | |
| "Randomizing move properties...".Dump(); | |
| bool KeepOriginalType(ushort move) => move is 449 or 686 or 851; | |
| foreach (var t in MoveInfoEdited.Table.Where(z => z.CanUseMove && z.MoveID != 0)) | |
| { | |
| if (RANDOMIZE_MOVE_PROPERTIES_ACCURACY && !IsStatusMove(t.Category)) | |
| t.Accuracy = GetRandomMoveAccuracy(); | |
| if (RANDOMIZE_MOVE_PROPERTIES_CATEGORY && !IsStatusMove(t.Category)) | |
| t.Category = (byte)GetRandom(1, 3); | |
| if (RANDOMIZE_MOVE_PROPERTIES_POWER && !IsStatusMove(t.Category) && t.Power > 1) | |
| t.Power = GetRandomMovePower(); | |
| if (RANDOMIZE_MOVE_PROPERTIES_PP && t.PP != 1) | |
| t.PP = (byte)pp[GetRandom(pp.Length)]; | |
| if (RANDOMIZE_MOVE_PROPERTIES_TYPE && !KeepOriginalType(t.MoveID)) | |
| t.Type = GetRandomType(); | |
| } | |
| SpoilerLog.Add("=========================================================================================="); | |
| SpoilerLog.Add($"| {localeMove, -28} | {localeType, -10} | {localeCategory, -9} | {localePower, -9} | {localeAccuracy, -11} | {localePP, -4} |"); | |
| SpoilerLog.Add("=========================================================================================="); | |
| foreach (var t in MoveInfoEdited.Table.Where(z => z.CanUseMove && z.MoveID != 0)) | |
| { | |
| var pow = t.Power is 0 or 1 ? "———" : $"{t.Power}"; | |
| var acc = t.Accuracy == 101 ? "———" : $"{t.Accuracy}"; | |
| SpoilerLog.Add($"| {StringsMove[t.MoveID], -28} | {StringsType[(byte)t.Type], -10} | {categories[Languages.IndexOf(LANGUAGE)][t.Category], -9} | {pow, -9} | {acc, -11} | {t.PP, -4} |"); | |
| } | |
| SpoilerLog.Add("=========================================================================================="); | |
| SpoilerLog.Add(""); | |
| UpdateMoveTable(); | |
| } | |
| } | |
| private static byte GetRandomMovePower() => GetRandom(100) switch | |
| { | |
| <= 99 and >= 65 => 40, // 35% | |
| <= 64 and >= 40 => 60, // 25% | |
| <= 39 and >= 20 => 80, // 20% | |
| <= 19 and >= 10 => 100, // 10% | |
| <= 9 and >= 5 => 120, // 5% | |
| <= 4 and >= 2 => 20, // 3% | |
| 1 => 10, // 1% | |
| _ => 150, // 1% | |
| }; | |
| private static byte GetRandomMoveAccuracy() => GetRandom(100) switch | |
| { | |
| <= 99 and >= 50 => 100, // 50% | |
| <= 49 and >= 30 => 90, // 20% | |
| <= 29 and >= 20 => 80, // 10% | |
| <= 19 and >= 10 => 70, // 10% | |
| <= 9 and >= 5 => 50, // 5% | |
| <= 4 and >= 2 => 30, // 3% | |
| 1 => 10, // 1% | |
| _ => 101, // 1% | |
| }; | |
| private static void RandomizeTradeEncounters() | |
| { | |
| "Randomizing in-game trades...".Dump(); | |
| const string file = "eventTradePokemon_array.bin"; | |
| var dest = Path.Combine(DEST_MASTER, "world/data/event/eventTradePokemon/"); | |
| var path = Path.Combine(PATH_MASTER, "world/data/event/eventTradePokemon/"); | |
| var obj = FlatBufferConverter.DeserializeFrom<EventTradePokemonArray>(Path.Combine(path, file)); | |
| var coaches = new[] | |
| { | |
| string.Empty, | |
| string.Empty, | |
| "bb4kings_honoo", | |
| "gym_nomal", | |
| "paldea_botan", | |
| "paldea4kings_zimen", | |
| "teacher_head", | |
| "gym_koori", | |
| "gym_mizu", | |
| "teacher_dragon", | |
| "gym_mushi", | |
| "bb4kings_dragon", | |
| "teacher_battle", | |
| "gym_kusa", | |
| "teacher_health", | |
| "gym_denki", | |
| "paldea_rival", | |
| "bb4kings_hagane", | |
| "paldea_top", | |
| "paldea_friend", | |
| "paldea4kings_hagane", | |
| "gym_ghost", | |
| "teacher_history", | |
| "gym_esper", | |
| "teacher_home", | |
| "teacher_language", | |
| "bbteacher_head", | |
| "bb_brother", | |
| "bb4kings_fairy", | |
| "teacher_math", | |
| "bb_sister", | |
| "teacher_ecology", | |
| string.Empty, | |
| }; | |
| bool IsBlueberryTrade(string id) => id.Contains("trade_gest"); | |
| for (int i = 0; i < obj.Table.Count; i++) | |
| { | |
| var t = obj.Table[i].PokeData; | |
| var key = obj.Table[i].Label; | |
| var oldSpecies = t.DevId; | |
| if (RANDOMIZE_POKEMON_TO_HAVE_SIMILAR_STRENGTH) | |
| { | |
| var pk = GetSimilarStrengthPokemon(t.DevId, t.FormId); | |
| t.DevId = pk.Species; | |
| t.FormId = pk.Form; | |
| } | |
| else | |
| { | |
| t.DevId = GetRandomSpecies(t.DevId); | |
| t.FormId = GetRandomForm(t.DevId); | |
| } | |
| t.Sex = GetRandomGender(t.DevId, t.FormId); | |
| t.Tokusei = GetRandomAbilitySlot(); | |
| t.GemType = GetTeraType(t.DevId, t.FormId, true); | |
| t.RareType = t.RareType == RareType.RARE ? RareType.RARE : RareType.DEFAULT; | |
| t.Seikaku = t.SeikakuHosei = GetRandomNature(t.DevId, t.FormId); | |
| t.WazaType = WazaType.DEFAULT; | |
| t.Waza1 = GetCleanMoveSlot(); | |
| t.Waza2 = GetCleanMoveSlot(); | |
| t.Waza3 = GetCleanMoveSlot(); | |
| t.Waza4 = GetCleanMoveSlot(); | |
| if (GIVE_POKEMON_RANDOM_POKE_BALLS) | |
| t.BallId = GetRandomBall(); | |
| if (GIVE_POKEMON_RANDOM_RIBBONS_OR_MARKS) | |
| t.SetRibbon = GetRandomRibbonOrMark(); | |
| if (MODIFY_POKEMON_LEVELS) | |
| t.Level = GetModifiedLevel(t.Level); | |
| if (GIVE_POKEMON_RANDOM_HELD_ITEMS) | |
| t.Item = GetRandomItem(); | |
| if (FORCE_FULLY_EVOLVED_POKEMON && t.Level >= FORCE_FULLY_EVOLVED_POKEMON_LEVEL) | |
| { | |
| var pk = ForceEvolveSpeciesFully(t.DevId, t.FormId); | |
| t.DevId = pk.Species; | |
| t.FormId = pk.Form; | |
| t.Sex = GetRandomGender(t.DevId, t.FormId); // refresh | |
| // refresh Toxtricity nature | |
| if (t.DevId == DevID.DEV_SUTORINDAA) | |
| t.Seikaku = GetRandomNature(t.DevId, t.FormId); | |
| } | |
| if (IsFormHeldItemDependent(t.DevId, t.FormId)) | |
| t.Item = GetFormSpecificItem(t.DevId, t.FormId); | |
| if (GIVE_ALL_POKEMON_MAX_INDIVIDUAL_VALUES) | |
| { | |
| t.TalentType = TalentType.VALUE; | |
| t.TalentValue = FlawlessIVSpread; | |
| t.TalentVnum = 0; | |
| } | |
| if (IsBlueberryTrade(key)) | |
| { | |
| t.UseNickName = false; | |
| FixTradeTextBlueberry(coaches[i], (ushort)oldSpecies, (ushort)t.DevId); | |
| } | |
| } | |
| if (!Directory.Exists(dest)) | |
| Directory.CreateDirectory(dest); | |
| var data = FlatBufferConverter.SerializeFrom(obj); | |
| File.WriteAllBytes(Path.Combine(dest, file), data); | |
| RandomizeTradeRequirements(obj.Table); | |
| } | |
| private static void RandomizeTradeRequirements(IList<EventTradePokemon> give) | |
| { | |
| const string file = "eventTradeList_array.bin"; | |
| var dest = Path.Combine(DEST_MASTER, "world/data/event/eventTradeList/"); | |
| var path = Path.Combine(PATH_MASTER, "world/data/event/eventTradeList/"); | |
| var obj = FlatBufferConverter.DeserializeFrom<EventTradeListArray>(Path.Combine(path, file)); | |
| foreach (var t in obj.Table) | |
| { | |
| t.SendPokeDevId = GetRandomSpecies(t.SendPokeDevId); | |
| t.SendPokeFormId = -1; // be permissive, allow any form | |
| } | |
| if (GENERATE_SPOILER_LOG_FILE) | |
| WriteTradeChangesToLog(give, obj.Table); | |
| if (!Directory.Exists(dest)) | |
| Directory.CreateDirectory(dest); | |
| var data = FlatBufferConverter.SerializeFrom(obj); | |
| File.WriteAllBytes(Path.Combine(dest, file), data); | |
| FixTradeTextPaldea(give, obj.Table); | |
| } | |
| private static void FixTradeTextPaldea(IList<EventTradePokemon> give, IList<EventTradeList> take) | |
| { | |
| var give1 = (ushort)give[00].PokeData.DevId; | |
| var give2 = (ushort)give[01].PokeData.DevId; | |
| var give3 = (ushort)give[32].PokeData.DevId; | |
| var take1 = (ushort)take[00].SendPokeDevId; | |
| var take2 = (ushort)take[01].SendPokeDevId; | |
| var take3 = (ushort)take[02].SendPokeDevId; | |
| // in the event that one half of the trade randomizes into one of the original species, we swap strings in two steps | |
| const string TRADE_GIVE = "GIVE_POKEMON"; | |
| const string TRADE_TAKE = "TAKE_POKEMON"; | |
| foreach (var lang in LanguagesToIterate) | |
| { | |
| var dest = Path.Combine(DEST_MASTER, $"message/dat/{lang}/script/"); | |
| var lines = GetStringsScript(lang, "field_trade"); | |
| var names = GetStringsCommon(lang, "monsname"); | |
| var flags = GetStringFlagsScript(lang, "field_trade"); | |
| var TRADE_A = names[0194]; // Wooper | |
| var TRADE_B_1 = names[0093]; // Haunter | |
| var TRADE_B_2 = names[0871]; // Pincurchin | |
| var TRADE_C_1 = names[0872]; // Snom | |
| var TRADE_C_2 = names[0669]; // Flabébé | |
| // special handling for Wooper -> Wooper | |
| List<int> WooperStrings = []; | |
| var index = lines[09].IndexOf(TRADE_A); | |
| while (index != -1) | |
| { | |
| WooperStrings.Add(index); | |
| index = lines[09].IndexOf(TRADE_A, index + 1); | |
| } | |
| if (lang == "English") | |
| lines[09] = lines[09].Remove(WooperStrings[2], TRADE_A.Length).Insert(WooperStrings[2], names[take1]); | |
| if (lang != "Italian") | |
| lines[09] = lines[09].Remove(WooperStrings[1], TRADE_A.Length).Insert(WooperStrings[1], names[give1]); | |
| lines[09] = lines[09].Remove(WooperStrings[0], TRADE_A.Length).Insert(WooperStrings[0], names[take1]); | |
| // Wooper -> Wooper | |
| lines[10] = lines[10].Replace(TRADE_A, TRADE_TAKE); | |
| lines[10] = lines[10].Replace(TRADE_TAKE, names[take1]); | |
| lines[11] = lines[11].Replace(TRADE_A, TRADE_TAKE); | |
| lines[11] = lines[11].Replace(TRADE_TAKE, names[take1]); | |
| // grammar | |
| if (lang == "English" && GetStartsWithVowel(names[take1])) | |
| lines[11] = lines[11].Replace($"a {names[take1]}", $"an {names[take1]}"); | |
| lines[12] = lines[12].Replace(TRADE_A, TRADE_GIVE); | |
| lines[12] = lines[12].Replace(TRADE_GIVE, names[give1]); | |
| lines[13] = lines[13].Replace(TRADE_A, TRADE_TAKE); | |
| lines[13] = lines[13].Replace(TRADE_TAKE, names[take1]); | |
| for (int t = 0; t < lines.Length; t++) | |
| { | |
| // Pincurchin -> Haunter | |
| lines[t] = lines[t].Replace(TRADE_B_2, TRADE_TAKE).Replace(TRADE_B_1, TRADE_GIVE); | |
| lines[t] = lines[t].Replace(TRADE_TAKE, names[take2]).Replace(TRADE_GIVE, names[give2]); | |
| // grammar | |
| if (lang == "English") | |
| { | |
| if (lines[t].Contains($"A {names[take2]}") && GetStartsWithVowel(names[take2])) lines[t] = lines[t].Replace($"A {names[take2]}", $"An {names[take2]}"); | |
| if (lines[t].Contains($"a {names[take2]}") && GetStartsWithVowel(names[take2])) lines[t] = lines[t].Replace($"a {names[take2]}", $"an {names[take2]}"); | |
| if (lines[t].Contains($"A {names[give2]}") && GetStartsWithVowel(names[give2])) lines[t] = lines[t].Replace($"A {names[give2]}", $"An {names[give2]}"); | |
| if (lines[t].Contains($"a {names[give2]}") && GetStartsWithVowel(names[give2])) lines[t] = lines[t].Replace($"a {names[give2]}", $"an {names[give2]}"); | |
| } | |
| // Flabébé -> Snom | |
| lines[t] = lines[t].Replace(TRADE_C_2, TRADE_TAKE).Replace(TRADE_C_1, TRADE_GIVE); | |
| lines[t] = lines[t].Replace(TRADE_TAKE, names[take3]).Replace(TRADE_GIVE, names[give3]); | |
| // grammar | |
| if (lang == "English") | |
| { | |
| if (lines[t].Contains($"A {names[take3]}") && GetStartsWithVowel(names[take3])) lines[t] = lines[t].Replace($"A {names[take3]}", $"An {names[take3]}"); | |
| if (lines[t].Contains($"a {names[take3]}") && GetStartsWithVowel(names[take3])) lines[t] = lines[t].Replace($"a {names[take3]}", $"an {names[take3]}"); | |
| if (lines[t].Contains($"A {names[give3]}") && GetStartsWithVowel(names[give3])) lines[t] = lines[t].Replace($"A {names[give3]}", $"An {names[give3]}"); | |
| if (lines[t].Contains($"a {names[give3]}") && GetStartsWithVowel(names[give3])) lines[t] = lines[t].Replace($"a {names[give3]}", $"an {names[give3]}"); | |
| } | |
| } | |
| if (!Directory.Exists(dest)) | |
| Directory.CreateDirectory(dest); | |
| var write = TextFile.GetBytes(lines, flags, TextConfig); | |
| File.WriteAllBytes(Path.Combine(dest, "field_trade.dat"), write); | |
| } | |
| } | |
| private static void FixTradeTextBlueberry(string coach, ushort oldSpecies, ushort newSpecies) | |
| { | |
| bool IsGrammarCheckOriginallyAn(string coach) => coach == "teacher_dragon"; | |
| bool IsGrammarCheckOriginallyA(string coach) => coach is "bb4kings_honoo" or "gym_nomal" or "paldea_botan" or "paldea4kings_zimen" | |
| or "teacher_head" or "gym_koori" or "gym_mushi" or "teacher_battle" | |
| or "teacher_health" or "paldea_rival" or "bb4kings_hagane" or "paldea_top" or "teacher_math"; | |
| var botan = new[] { "Veevee", "Volivoli", "Evoli", "Vivì", "ブイ", "ブイ", "브이브이", "布布", "Vivi", "布布" }; | |
| var ghost = new[] { "DJ G-Rave", "Toutombe", "Gruff", "DJ G-Rave", "DJ BOCHI", "DJ BOCHI", "DJ MANGMANG-E", "DJ MUZAIGOU", "DJ Nitxo", "DJ GHOSTDOG" }; | |
| var entries = GetEntryListAHTBScript(LANGUAGE, $"coach_{coach}"); | |
| // text revealing the offered Pokémon | |
| var revealKey = coach is "teacher_history" or "bbteacher_head" ? "trade_01_02" : "trade_01_03"; | |
| var revealHash = FnvHash.HashFnv1a_64($"coach_{coach}_{revealKey}"); | |
| var revealTarget = entries.FirstOrDefault(z => z.Hash == revealHash); | |
| var revealIndex = entries.IndexOf(revealTarget); | |
| // text confirming the trade | |
| var confirmHash = FnvHash.HashFnv1a_64($"coach_{coach}_trade_02_00"); | |
| var confirmTarget = entries.FirstOrDefault(z => z.Hash == confirmHash); | |
| var confirmIndex = entries.IndexOf(confirmTarget); | |
| // text after the trade | |
| var afterKey = coach == "bbteacher_head" ? "trade_01_04" : "randomtalk_04"; | |
| var afterHash = FnvHash.HashFnv1a_64($"coach_{coach}_{afterKey}"); | |
| var afterTarget = entries.FirstOrDefault(z => z.Hash == afterHash); | |
| var afterIndex = entries.IndexOf(afterTarget); | |
| foreach (var lang in LanguagesToIterate) | |
| { | |
| var langIndex = Languages.IndexOf(lang); | |
| var dest = Path.Combine(DEST_MASTER, $"message/dat/{lang}/script/"); | |
| var names = GetStringsCommon(lang, "monsname"); | |
| var lines = GetStringsScript(lang, $"coach_{coach}"); | |
| var flags = GetStringFlagsScript(lang, $"coach_{coach}"); | |
| var newName = names[newSpecies]; | |
| var oldName = coach switch | |
| { | |
| "paldea_botan" => botan[langIndex], // Veevee | |
| "gym_ghost" => ghost[langIndex], // DJ G-Rave | |
| _ => names[oldSpecies], | |
| }; | |
| lines[revealIndex] = lines[revealIndex].Replace(oldName, newName); | |
| lines[confirmIndex] = lines[confirmIndex].Replace(oldName, newName); | |
| lines[afterIndex] = lines[afterIndex].Replace(oldName, newName); | |
| // refers to it both as Veevee and Eevee in the same line | |
| if (coach == "paldea_botan" && lang == "English") | |
| lines[afterIndex] = lines[afterIndex].Replace(oldName, newName); | |
| // grammar: an -> a | |
| if (IsGrammarCheckOriginallyAn(coach) && !GetStartsWithVowel(newName) && lang == "English") | |
| lines[revealIndex] = lines[revealIndex].Replace($"an {newName}", $"a {newName}"); | |
| // grammar: a -> an | |
| if (IsGrammarCheckOriginallyA(coach) && GetStartsWithVowel(newName) && lang == "English") | |
| { | |
| // line break in the middle | |
| if (coach == "teacher_head") | |
| lines[revealIndex] = lines[revealIndex].Replace($"a\\n{newName}", $"an\\n{newName}"); | |
| else | |
| lines[revealIndex] = lines[revealIndex].Replace($"a {newName}", $"an {newName}"); | |
| } | |
| // some languages refer to Greavard as one of two things, depending on the point in dialogue | |
| if (coach == "gym_ghost") | |
| { | |
| if (lang == "German") | |
| lines[afterIndex] = lines[afterIndex].Replace("DJ G-Rave", newName); | |
| if (lang is "JPN" or "JPN_KANJI") | |
| lines[confirmIndex] = lines[confirmIndex].Replace("BOCHI", newName); | |
| if (lang == "Korean") | |
| lines[confirmIndex] = lines[confirmIndex].Replace("MANGMANG-E", newName); | |
| if (lang == "Simp_Chinese") | |
| lines[confirmIndex] = lines[confirmIndex].Replace("MUZAIGOU", newName); | |
| if (lang == "Trad_Chinese") | |
| lines[confirmIndex] = lines[confirmIndex].Replace("GHOSTDOG", newName); | |
| } | |
| var write = TextFile.GetBytes(lines, flags, TextConfig); | |
| File.WriteAllBytes(Path.Combine(dest, $"coach_{coach}.dat"), write); | |
| } | |
| } | |
| private static List<EventTradePokemon> GetOldTradeEncounters() | |
| { | |
| const string file = "eventTradePokemon_array.bin"; | |
| var path = Path.Combine(PATH_MASTER, "world/data/event/eventTradePokemon/"); | |
| var obj = FlatBufferConverter.DeserializeFrom<EventTradePokemonArray>(Path.Combine(path, file)); | |
| return obj.Table.Select(z => z).ToList(); | |
| } | |
| private static void RandomizeAcademyAceTournamentRewards() | |
| { | |
| "Randomizing Academy Ace Tournament item rewards...".Dump(); | |
| const string file = "sub_012_reward_array.bin"; | |
| var dest = Path.Combine(DEST_MASTER, "world/data/event/sub_012/sub_012_reward/"); | |
| var path = Path.Combine(PATH_MASTER, "world/data/event/sub_012/sub_012_reward/"); | |
| var obj = FlatBufferConverter.DeserializeFrom<RewardTableArray>(Path.Combine(path, file)); | |
| List<(ItemID Item, int Quantity, float Rate)> OldItems = []; | |
| List<(ItemID Item, int Quantity, float Rate)> NewItems = []; | |
| List<ItemID> Used = []; | |
| float totalRate = 0; | |
| foreach (var t in obj.Table) | |
| totalRate += t.LotteryWeight; | |
| var ordered = obj.Table.OrderByDescending(z => z.LotteryWeight); | |
| foreach (var t in ordered.Where(z => z.ItemId != 0)) | |
| { | |
| float rate = (float)(Math.Round((t.LotteryWeight / totalRate) * 100.0f)); | |
| OldItems.Add((t.ItemId, t.ItemNum, rate)); | |
| t.ItemId = GetRandomItem(); | |
| // don't allow duplicates | |
| if (Used.Contains(t.ItemId)) | |
| t.ItemId = GetRandomItem(); | |
| // don't give excessive quantities | |
| if (ValuableItems.Contains((ushort)t.ItemId)) | |
| t.ItemNum = 1; | |
| NewItems.Add((t.ItemId, t.ItemNum, rate)); | |
| Used.Add(t.ItemId); | |
| } | |
| if (GENERATE_SPOILER_LOG_FILE) | |
| { | |
| SpoilerLog.Add("== ACADEMY ACE TOURNAMENT REWARDS =="); | |
| SpoilerLog.Add("================================================================================"); | |
| SpoilerLog.Add("| Rate | Old Item | New Item |"); | |
| SpoilerLog.Add("================================================================================"); | |
| for (int i = 0; i < NewItems.Count; i++) | |
| { | |
| var oldItem = OldItems[i]; | |
| var newItem = NewItems[i]; | |
| var oldItemDisplay = $"{StringsItem[(ushort)oldItem.Item]} (×{oldItem.Quantity})"; | |
| var newItemDisplay = $"{StringsItem[(ushort)newItem.Item]} (×{newItem.Quantity})"; | |
| var rate = NewItems[i].Rate; | |
| SpoilerLog.Add($"| {rate:00.00}% | {oldItemDisplay, -32} | {newItemDisplay, -32} |"); | |
| } | |
| SpoilerLog.Add("================================================================================"); | |
| SpoilerLog.Add(""); | |
| } | |
| if (!Directory.Exists(dest)) | |
| Directory.CreateDirectory(dest); | |
| var data = FlatBufferConverter.SerializeFrom(obj); | |
| File.WriteAllBytes(Path.Combine(dest, file), data); | |
| } | |
| private static void RandomizePokedexMilestoneRewards() | |
| { | |
| "Randomizing Pokédex milestone rewards...".Dump(); | |
| var rewardData = new[] | |
| { | |
| "world/data/ui/pokedex/reward_data/reward_data_array.bin", | |
| "world/data/ui/pokedex/reward_data_dlc1/reward_data_dlc1_array.bin", | |
| "world/data/ui/pokedex/reward_data_dlc2/reward_data_dlc2_array.bin", | |
| }; | |
| foreach (var r in rewardData) | |
| { | |
| var index = r.LastIndexOf("/") + 1; | |
| var f = r[index..]; | |
| var d = r[..index]; | |
| var rewardsIndex = rewardData.IndexOf(r); | |
| var entry = rewardsIndex == 0 ? "leaguecard_02_02" : rewardsIndex == 1 ? "leaguecard_11_00" : "leaguecard_12_00"; | |
| var dex = GetStringFromAHTBCommon(LANGUAGE, "leaguecard", FnvHash.HashFnv1a_64(entry)).ToUpper(); | |
| int GetItemQuantity(int num) => num switch | |
| { | |
| >= 130 and <= 240 => 3, | |
| >= 250 and <= 360 => 5, | |
| >= 370 and <= 480 => 8, | |
| >= 490 and <= 600 => 10, | |
| >= 610 and <= 750 => 15, | |
| _ => 1, | |
| }; | |
| int GetCaptureRequirement(int index) => index switch | |
| { | |
| 730 => 723, | |
| 740 => 726, | |
| 750 => 729, | |
| _ => index, | |
| }; | |
| var file = Path.Combine(PATH_MASTER, d, f); | |
| var obj = FlatBufferConverter.DeserializeFrom<RewardDataArray>(file); | |
| // since we expanded the Paldea Pokédex, we need to adjust the milestones to account for 733 available species | |
| // each page on the UI must have four milestones, and since 733 is not evenly divisible by 4, we need to make the final few milestones smaller | |
| if (f == "reward_data_array.bin") | |
| { | |
| obj.Table.Clear(); | |
| for (int i = 10; i <= 750; i += 10) | |
| { | |
| var req = GetCaptureRequirement(i); | |
| var reward = new RewardData() | |
| { | |
| PokedexType = PokedexType.TITAN, | |
| CaptureNum = req, | |
| ItemId = GetRandomItem(), | |
| ItemNum = GetItemQuantity(i), | |
| }; | |
| obj.Table.Add(reward); | |
| } | |
| // for the final reward, award 100 of a random valuable item, but if it's Gimmighoul Coins, give 999 of them | |
| var item = (ItemID)ValuableItems[GetRandom(ValuableItems.Count)]; | |
| var num = item == ItemID.ITEMID_SOZAI182 ? 999 : 100; | |
| var finalReward = new RewardData() | |
| { | |
| PokedexType = PokedexType.TITAN, | |
| CaptureNum = 733, | |
| ItemId = item, | |
| ItemNum = num, | |
| }; | |
| obj.Table.Add(finalReward); | |
| } | |
| else | |
| { | |
| var valuable = (ItemID)ValuableItems[GetRandom(ValuableItems.Count)]; | |
| foreach (var t in obj.Table) | |
| { | |
| bool last = obj.Table.IndexOf(t) == obj.Table.Count - 1; // valuable item if last milestone, random if not | |
| t.ItemId = last ? valuable : GetRandomItem(); | |
| t.ItemNum = last ? t.ItemId == ItemID.ITEMID_SOZAI182 ? 333 : 3 : 1; | |
| } | |
| } | |
| SpoilerLog.Add($"== POKÉDEX MILESTONE REWARDS ({dex}) =="); | |
| foreach (var t in obj.Table) | |
| SpoilerLog.Add($"- {t.CaptureNum} Captured: {StringsItem[(ushort)t.ItemId]} (×{t.ItemNum})"); | |
| SpoilerLog.Add(""); | |
| var data = FlatBufferConverter.SerializeFrom(obj); | |
| var dest = Path.Combine(DEST_MASTER, d); | |
| if (!Directory.Exists(dest)) | |
| Directory.CreateDirectory(dest); | |
| File.WriteAllBytes(Path.Combine(dest, f), data); | |
| } | |
| } | |
| private static void RandomizeStarBarrageEncounters() | |
| { | |
| "Randomizing Star Barrage encounters...".Dump(); | |
| const string file = "AjitoPokemon_array.bin"; | |
| var dest = Path.Combine(DEST_MASTER, "world/data/ajito/AjitoPokemon/"); | |
| var path = Path.Combine(PATH_MASTER, "world/data/ajito/AjitoPokemon/"); | |
| var obj = FlatBufferConverter.DeserializeFrom<AjitoPokemonArray>(Path.Combine(path, file)); | |
| foreach (var enc in obj.Table) | |
| { | |
| var t = enc.Table; | |
| if (RANDOMIZE_POKEMON_TO_HAVE_SIMILAR_STRENGTH) | |
| { | |
| var pk = GetSimilarStrengthPokemon(t.DevId, (short)t.FormNo); | |
| t.DevId = pk.Species; | |
| t.FormNo = pk.Form; | |
| } | |
| else | |
| { | |
| t.DevId = GetRandomSpecies(t.DevId); | |
| t.FormNo = GetRandomForm(t.DevId); | |
| } | |
| if (TYPE_THEMED_IMPORTANT_TRAINERS != TypeThemeRandomizer.NotTypeThemed && t.PokemonId != 0) | |
| { | |
| byte type = t.PokemonId switch | |
| { | |
| >= 001 and <= 100 => ThemedTypes[09], // Fire | |
| >= 101 and <= 200 => ThemedTypes[08], // Dark | |
| >= 201 and <= 300 => ThemedTypes[10], // Poison | |
| >= 301 and <= 400 => ThemedTypes[12], // Fighting | |
| >= 401 and <= 500 => ThemedTypes[11], // Fairy | |
| _ => throw new ArgumentException(nameof(t.PokemonId)), | |
| }; | |
| var pk = GetTypeThemedPokemon(type); | |
| t.DevId = pk.Species; | |
| t.FormNo = pk.Form; | |
| } | |
| } | |
| if (GENERATE_SPOILER_LOG_FILE) | |
| WriteStarBarrageChangesToLog(obj.Table); | |
| if (!Directory.Exists(dest)) | |
| Directory.CreateDirectory(dest); | |
| var data = FlatBufferConverter.SerializeFrom(obj); | |
| File.WriteAllBytes(Path.Combine(dest, file), data); | |
| ModifyStarBarrageParameters(); | |
| } | |
| private static List<AjitoPokemon> GetOldStarBarrageEncounters() | |
| { | |
| const string file = "AjitoPokemon_array.bin"; | |
| var path = Path.Combine(PATH_MASTER, "world/data/ajito/AjitoPokemon/"); | |
| var obj = FlatBufferConverter.DeserializeFrom<AjitoPokemonArray>(Path.Combine(path, file)); | |
| return obj.Table.Select(z => z).ToList(); | |
| } | |
| private static void ModifyStarBarrageParameters() | |
| { | |
| const string file = "AjitoCommonLevel_array.bin"; | |
| var dest = Path.Combine(DEST_MASTER, "world/data/ajito/AjitoCommonLevel/"); | |
| var path = Path.Combine(PATH_MASTER, "world/data/ajito/AjitoCommonLevel/"); | |
| var obj = FlatBufferConverter.DeserializeFrom<AjitoCommonLevelArray>(Path.Combine(path, file)); | |
| foreach (var enc in obj.Table) | |
| { | |
| var t = enc.Inner; | |
| if (t.ClearPokemonNum == 30) t.ClearPokemonNum = (int)(Math.Clamp((int)TARGET_STAR_BARRAGE_KO_COUNT_INITIAL, 1, 99)); | |
| if (t.ClearPokemonNum == 50) t.ClearPokemonNum = (int)(Math.Clamp((int)TARGET_STAR_BARRAGE_KO_COUNT_REMATCH, 1, 99)); | |
| if (MODIFY_POKEMON_LEVELS) | |
| { | |
| t.MinPokemonLv = GetModifiedLevel(t.MinPokemonLv); | |
| t.MaxPokemonLv = GetModifiedLevel(t.MaxPokemonLv); | |
| } | |
| } | |
| if (!Directory.Exists(dest)) | |
| Directory.CreateDirectory(dest); | |
| var data = FlatBufferConverter.SerializeFrom(obj); | |
| File.WriteAllBytes(Path.Combine(dest, file), data); | |
| } | |
| private static void RandomizeStarmobileStats() | |
| { | |
| SpoilerLog.Add("== STARMOBILE STATS =="); | |
| "Randomizing Starmobile stats...".Dump(); | |
| const string file = "dan_car_battle_data_array.bin"; | |
| var dest = Path.Combine(DEST_MASTER, "world/data/danbattle/boss/dan_car_battle_data/"); | |
| var path = Path.Combine(PATH_MASTER, "world/data/danbattle/boss/dan_car_battle_data/"); | |
| var obj = FlatBufferConverter.DeserializeFrom<DanCarBattleDataArray>(Path.Combine(path, file)); | |
| var level = GetLevelAbbreviation(LANGUAGE); | |
| var cars = GetStringsScript(LANGUAGE, "futatsuna"); | |
| var localeType = GetStringFromAHTBCommon(LANGUAGE, "status", FnvHash.HashFnv1a_64("msg_ui_status_pokemon_type_00")); | |
| var localeAbility = GetStringFromAHTBCommon(LANGUAGE, "status", FnvHash.HashFnv1a_64("msg_ui_status_pokemon_chara_00")); | |
| var localeMoves = new[] { "Moves", "Capacités", "Attacken", "Mosse", "わざ", "わざ", "기술", "招式", "Movimiento", "招式" }; | |
| var starmobileNames = cars[18..23].ToList(); | |
| foreach (var car in obj.Table) | |
| { | |
| var t = car.DanBossCarBattleStruct; | |
| t.TokuseiId = (TokuseiID)GetRandomAbility(); | |
| t.Type1 = t.Type2 = TYPE_THEMED_IMPORTANT_TRAINERS == TypeThemeRandomizer.NotTypeThemed ? (MoveType)GetRandomType() : t.TrainerId switch | |
| { | |
| "dan_aku_boss_01" => (MoveType)ThemedTypes[08], // Dark | |
| "dan_hono_boss_01" => (MoveType)ThemedTypes[09], // Fire | |
| "dan_doku_boss_01" => (MoveType)ThemedTypes[10], // Poison | |
| "dan_fairy_boss_01" => (MoveType)ThemedTypes[11], // Fairy | |
| "dan_kakutou_boss_01" => (MoveType)ThemedTypes[12], // Fighting | |
| _ => throw new ArgumentException(nameof(t.TrainerId), t.TrainerId), | |
| }; | |
| if (ALLOW_DUAL_TYPE_STARMOBILES && GetRandom(100) < RANDOM_DUAL_TYPE_CHANCE_PERCENT) | |
| { | |
| while (t.Type2 == t.Type1) | |
| t.Type2 = (MoveType)GetRandomType(); | |
| } | |
| List<int> Moves = []; | |
| if (RANDOMIZE_POKEMON_LEARNSETS == LearnsetRandomizer.MetronomeOnly) | |
| { | |
| Moves = [ 118, 118, 118, 118 ]; // fill all 4 move slots with Metronome, in case only 1 move breaks the AI | |
| } | |
| else if (TYPE_THEMED_IMPORTANT_TRAINERS != TypeThemeRandomizer.NotTypeThemed || RANDOMIZE_LEARNSETS_WITH_TYPE_BIAS && GetRandom(100) < RANDOM_LEARNSET_TYPE_BIAS_PERCENT) | |
| { | |
| // single type | |
| if (t.Type1 == t.Type2) | |
| { | |
| Moves = GetRandomMoveListWithBias(0, 0, (byte)t.Type1, (byte)t.Type2); | |
| } | |
| // dual-type, special handling to give 2 moves of each type | |
| else | |
| { | |
| var type1 = GetRandomMoveListWithBias(0, 0, (byte)t.Type1, (byte)t.Type1).Take(2).ToList(); | |
| var type2 = GetRandomMoveListWithBias(0, 0, (byte)t.Type2, (byte)t.Type2).Take(2).ToList(); | |
| Moves = type1.Concat(type2).ToList(); | |
| } | |
| } | |
| else | |
| { | |
| Moves = GetRandomMoveList(); | |
| } | |
| t.Waza1 = (WazaID)Moves[0]; | |
| t.Waza2 = (WazaID)Moves[1]; | |
| t.Waza3 = (WazaID)Moves[2]; | |
| t.Waza4 = (WazaID)Moves[3]; | |
| if (MODIFY_POKEMON_LEVELS) | |
| t.Level = GetModifiedLevel(t.Level); | |
| var ability = IsAbilityVague((ushort)t.TokuseiId) ? GetDescriptiveAbilityName((ushort)t.TokuseiId, StringsAbility, StringsSpecies, StringsItem) : StringsAbility[(ushort)t.TokuseiId]; | |
| var type = t.Type1 == t.Type2 ? StringsType[(byte)t.Type1] : $"{StringsType[(byte)t.Type1]}{TextSeparator}{StringsType[(byte)t.Type2]}"; | |
| SpoilerLog.Add($"{starmobileNames[obj.Table.IndexOf(car)]} ({level}{t.Level})"); | |
| SpoilerLog.Add($"{localeType}: {type}"); | |
| SpoilerLog.Add($"{localeAbility}: {ability}"); | |
| SpoilerLog.Add($"{localeMoves[Languages.IndexOf(LANGUAGE)]}:"); | |
| SpoilerLog.Add($"- {StringsMove[(ushort)t.Waza1]}"); | |
| if (RANDOMIZE_POKEMON_LEARNSETS != LearnsetRandomizer.MetronomeOnly) | |
| { | |
| SpoilerLog.Add($"- {StringsMove[(ushort)t.Waza2]}"); | |
| SpoilerLog.Add($"- {StringsMove[(ushort)t.Waza3]}"); | |
| SpoilerLog.Add($"- {StringsMove[(ushort)t.Waza4]}"); | |
| } | |
| SpoilerLog.Add(""); | |
| } | |
| UpdateStarmobileText(obj.Table); | |
| if (!Directory.Exists(dest)) | |
| Directory.CreateDirectory(dest); | |
| var data = FlatBufferConverter.SerializeFrom(obj); | |
| File.WriteAllBytes(Path.Combine(dest, file), data); | |
| } | |
| private static void UpdateStarmobileText(IList<DanCarBattleData> table) | |
| { | |
| foreach (var lang in LanguagesToIterate) | |
| { | |
| var source = File.ReadAllBytes(Path.Combine(DEST_MASTER, $"message/dat/{lang}/script/futatsuna.dat")); | |
| var bosses = new TextFile(source, TextConfig).Lines; | |
| var types = GetStringsCommon(lang, "typename"); | |
| var cars = bosses[18..23]; | |
| var flags = GetStringFlagsScript(lang, "futatsuna"); | |
| var localeType = GetStringFromAHTBCommon(lang, "status", FnvHash.HashFnv1a_64("msg_ui_status_pokemon_type_00")); | |
| var separator = GetTextSeparator(lang); | |
| for (int i = 0; i < table.Count; i++) | |
| { | |
| var t = table[i].DanBossCarBattleStruct; | |
| var car = cars[i]; | |
| var type = t.Type1 == t.Type2 ? types[(byte)t.Type1] : $"{types[(byte)t.Type1]}{separator}{types[(byte)t.Type2]}"; | |
| var line = $"{car} ({localeType}: {type})"; | |
| bosses[18 + i] = line; | |
| } | |
| var dest = Path.Combine(DEST_MASTER, $"message/dat/{lang}/script/"); | |
| if (!Directory.Exists(dest)) | |
| Directory.CreateDirectory(dest); | |
| var write = TextFile.GetBytes(bosses, flags, TextConfig); | |
| File.WriteAllBytes(Path.Combine(dest, "futatsuna.dat"), write); | |
| } | |
| } | |
| private static void RandomizeOgreOustinRewards() | |
| { | |
| "Randomizing Ogre Oustin' item rewards...".Dump(); | |
| const string fileA = "reward_action_param_array.bin"; | |
| var destA = Path.Combine(DEST_MASTER, "world/data/oniballoon/reward_action_param/"); | |
| var pathA = Path.Combine(PATH_MASTER, "world/data/oniballoon/reward_action_param/"); | |
| var objA = FlatBufferConverter.DeserializeFrom<RewardActionTableArray>(Path.Combine(pathA, fileA)); | |
| const string fileL = "reward_level_param_array.bin"; | |
| var destL = Path.Combine(DEST_MASTER, "world/data/oniballoon/reward_level_param/"); | |
| var pathL = Path.Combine(PATH_MASTER, "world/data/oniballoon/reward_level_param/"); | |
| var objL = FlatBufferConverter.DeserializeFrom<pkNX.Structures.FlatBuffers.SV.Balloon.RewardTableArray>(Path.Combine(pathL, fileL)); | |
| List<(ItemID Item, int Weight)> OldItemAction = []; | |
| List<(ItemID Item, int Weight)> NewItemAction = []; | |
| List<(ItemID Item, int Quantity)> OldItemLevel = []; | |
| List<(ItemID Item, int Quantity)> NewItemLevel = []; | |
| List<ItemID> Used = []; | |
| foreach (var t in objA.Table) | |
| { | |
| foreach (var item in t.Param.RewardItems.Where(z => z.ItemId != 0)) | |
| { | |
| OldItemAction.Add((item.ItemId, item.Rate)); | |
| item.ItemId = GetRandomItem(); | |
| // don't allow duplicates | |
| while (Used.Contains(item.ItemId)) | |
| item.ItemId = GetRandomItem(); | |
| // balanced weights | |
| if (ValuableItems.Contains((ushort)item.ItemId)) | |
| item.Rate = 2; | |
| NewItemAction.Add((item.ItemId, item.Rate)); | |
| Used.Add(item.ItemId); | |
| } | |
| } | |
| foreach (var t in objL.Table) | |
| { | |
| foreach (var item in t.Param.RewardItems.Where(z => z.ItemId != 0)) | |
| { | |
| OldItemLevel.Add((item.ItemId, item.Num)); | |
| item.ItemId = GetRandomItem(); | |
| // don't allow duplicates | |
| while (Used.Contains(item.ItemId)) | |
| item.ItemId = GetRandomItem(); | |
| // do not give excessive quantities | |
| if (ValuableItems.Contains((ushort)item.ItemId)) | |
| item.Num = 1; | |
| NewItemLevel.Add((item.ItemId, item.Num)); | |
| Used.Add(item.ItemId); | |
| } | |
| } | |
| if (GENERATE_SPOILER_LOG_FILE) | |
| { | |
| SpoilerLog.Add("== OGRE OUSTIN' ITEM REWARDS (RANDOM) =="); | |
| SpoilerLog.Add("======================================================================================="); | |
| SpoilerLog.Add("| Old Item | New Item | Weight |"); | |
| SpoilerLog.Add("======================================================================================="); | |
| for (int i = 0; i < NewItemAction.Count; i++) | |
| { | |
| var oldItem = OldItemAction[i]; | |
| var newItem = NewItemAction[i]; | |
| var weight = newItem.Weight; | |
| SpoilerLog.Add($"| {StringsItem[(ushort)oldItem.Item], -32} | {StringsItem[(ushort)newItem.Item], -32} | {weight.ToString(), -13} |"); | |
| } | |
| SpoilerLog.Add("======================================================================================="); | |
| SpoilerLog.Add(""); | |
| SpoilerLog.Add("== OGRE OUSTIN' ITEM REWARDS (GROUPED) =="); | |
| SpoilerLog.Add("======================================================================="); | |
| SpoilerLog.Add("| Old Item | New Item |"); | |
| SpoilerLog.Add("======================================================================="); | |
| for (int i = 0; i < NewItemLevel.Count; i++) | |
| { | |
| var oldItem = OldItemLevel[i]; | |
| var newItem = NewItemLevel[i]; | |
| var oldItemDisplay = $"{StringsItem[(ushort)oldItem.Item]} (×{oldItem.Quantity})"; | |
| var newItemDisplay = $"{StringsItem[(ushort)newItem.Item]} (×{newItem.Quantity})"; | |
| SpoilerLog.Add($"| {oldItemDisplay, -32} | {newItemDisplay, -32} |"); | |
| } | |
| SpoilerLog.Add("======================================================================="); | |
| SpoilerLog.Add(""); | |
| } | |
| if (!Directory.Exists(destA)) Directory.CreateDirectory(destA); | |
| if (!Directory.Exists(destL)) Directory.CreateDirectory(destL); | |
| var dataA = FlatBufferConverter.SerializeFrom(objA); | |
| var dataL = FlatBufferConverter.SerializeFrom(objL); | |
| File.WriteAllBytes(Path.Combine(destA, fileA), dataA); | |
| File.WriteAllBytes(Path.Combine(destL, fileL), dataL); | |
| } | |
| private static void RandomizeSpecialCoachRewards() | |
| { | |
| SpoilerLog.Add("== SPECIAL COACH ITEM REWARDS =="); | |
| "Randomizing Special Coach item rewards...".Dump(); | |
| const string file = "ClubNpcRewardList_array.bin"; | |
| var dest = Path.Combine(DEST_MASTER, "world/data/club/ClubNpcRewardList/"); | |
| var path = Path.Combine(PATH_MASTER, "world/data/club/ClubNpcRewardList/"); | |
| var obj = FlatBufferConverter.DeserializeFrom<ClubNpcRewardListArray>(Path.Combine(path, file)); | |
| var coaches = new[] { 015, 013, 009, 014, 007, 011, 010, 012, 008, 006, 034, 027, 029, 030, 033, 028, 005, 032, 031, 002, 016, 017, 001, 406, 405, 403, 404, 365, 364, 407 }; | |
| foreach (var t in obj.Table) | |
| { | |
| var reward1 = t.Reward01; | |
| var reward2 = t.Reward02; | |
| var reward3 = t.Reward03; | |
| if (reward1.RewardType != ClubRewardType.ITEM && reward2.RewardType != ClubRewardType.ITEM && reward3.RewardType != ClubRewardType.ITEM) | |
| continue; | |
| var line = "- " + StringsTrainerName[coaches[(byte)t.Coach]]; | |
| if (reward1.RewardType == ClubRewardType.ITEM) { reward1.RewardId = (ushort)GetRandomItem(); line += $": {StringsItem[(ushort)reward1.RewardId]} (×{reward1.Count})"; } | |
| if (reward2.RewardType == ClubRewardType.ITEM) { reward2.RewardId = (ushort)GetRandomItem(); line += $": {StringsItem[(ushort)reward2.RewardId]} (×{reward2.Count})"; } | |
| if (reward3.RewardType == ClubRewardType.ITEM) { reward3.RewardId = (ushort)GetRandomItem(); line += $": {StringsItem[(ushort)reward3.RewardId]} (×{reward3.Count})"; } | |
| SpoilerLog.Add(line); | |
| } | |
| SpoilerLog.Add(""); | |
| if (!Directory.Exists(dest)) | |
| Directory.CreateDirectory(dest); | |
| var data = FlatBufferConverter.SerializeFrom(obj); | |
| File.WriteAllBytes(Path.Combine(dest, file), data); | |
| } | |
| private static void RandomizePortoMarinadaAuctions() | |
| { | |
| "Randomizing Porto Marinada auction items...".Dump(); | |
| const string file = "gym_mizu_seri_item_array.bin"; | |
| var dest = Path.Combine(DEST_MASTER, "world/data/gym/gym_mizu_seri_item/"); | |
| var path = Path.Combine(PATH_MASTER, "world/data/gym/gym_mizu_seri_item/"); | |
| var obj = FlatBufferConverter.DeserializeFrom<SeriItemTableArray>(Path.Combine(path, file)); | |
| var specific = obj.Table.Where(z => z.DevId != 0).Select(z => z.ItemId).ToArray(); | |
| List<ItemID> OldAuctionItems = []; | |
| List<ItemID> Used = []; | |
| foreach (var t in obj.Table.Where(z => z.ItemId != 0 && z.DevId == 0)) | |
| { | |
| OldAuctionItems.Add(t.ItemId); | |
| t.ItemId = GetRandomItem(); | |
| // don't allow species-specific items in the general vendor auctions, and don't allow duplicates | |
| while (specific.Contains(t.ItemId) || Used.Contains(t.ItemId)) | |
| t.ItemId = GetRandomItem(); | |
| if (ValuableItems.Contains((ushort)t.ItemId)) | |
| { | |
| t.MinPrice = 650; | |
| t.BasePrice = 2000; | |
| t.MinItemNum = 1; | |
| t.MaxItemNum = 5; | |
| } | |
| else if (GetIsTeraShard((ushort)t.ItemId)) | |
| { | |
| t.MinPrice = 150; | |
| t.BasePrice = 400; | |
| t.MinItemNum = 50; | |
| t.MaxItemNum = 50; | |
| } | |
| else | |
| { | |
| t.MinPrice = 150; | |
| t.BasePrice = 400; | |
| t.MinItemNum = 5; | |
| t.MaxItemNum = 35; | |
| } | |
| Used.Add(t.ItemId); | |
| } | |
| // add auctions for Ogerpon's masks | |
| var ctr = 407; | |
| for (int i = 2406; i <= 2408; i++) | |
| { | |
| var entry = new SeriItemTable() | |
| { | |
| SeriItemTableId = ctr, | |
| ItemId = (ItemID)i, | |
| MinPrice = 500, | |
| BasePrice = 4000, | |
| MinItemNum = 1, | |
| MaxItemNum = 1, | |
| Venue4Probability = 1, | |
| BarkerMessageLabelName = "", | |
| ItemObjectName = "", | |
| BoxObjectName = "world/obj_template/parts/gym/mizu/box_object_01_/box_object_01.trsot", | |
| EffectName = "world/obj_template/parts/gym/mizu/ef_kirakira_01_/ef_kirakira_01.trsot", | |
| AcquiredFlagName = $"FEVT_SERI_LEGEND_{i:0000}", | |
| DevId = DevID.DEV_KAMENONI, | |
| }; | |
| obj.Table.Add(entry); | |
| ctr++; | |
| } | |
| if (GENERATE_SPOILER_LOG_FILE) | |
| { | |
| SpoilerLog.Add("== PORTO MARINADA AUCTION ITEMS =="); | |
| SpoilerLog.Add("======================================================================================="); | |
| SpoilerLog.Add("| Old Item | New Item | Quantity |"); | |
| SpoilerLog.Add("======================================================================================="); | |
| var sep = GetRangeSeparator(LANGUAGE); | |
| foreach (var t in obj.Table.Where(z => z.ItemId != 0 && z.DevId == 0)) | |
| { | |
| var index = obj.Table.IndexOf(t); | |
| var oldItem = OldAuctionItems[index]; | |
| var newItem = obj.Table[index]; | |
| var oldItemDisplay = StringsItem[(ushort)oldItem]; | |
| var newItemDisplay = StringsItem[(ushort)newItem.ItemId]; | |
| var quantity = newItem.MinItemNum == newItem.MaxItemNum ? $"{newItem.MaxItemNum}" : $"{newItem.MinItemNum}{sep}{newItem.MaxItemNum}"; | |
| SpoilerLog.Add($"| {oldItemDisplay, -32} | {newItemDisplay, -32} | {quantity, -13} |"); | |
| } | |
| SpoilerLog.Add("======================================================================================="); | |
| SpoilerLog.Add(""); | |
| } | |
| if (!Directory.Exists(dest)) | |
| Directory.CreateDirectory(dest); | |
| var data = FlatBufferConverter.SerializeFrom(obj); | |
| File.WriteAllBytes(Path.Combine(dest, file), data); | |
| } | |
| private static void RandomizeItemPrinterItems() | |
| { | |
| "Randomizing Item Printer items...".Dump(); | |
| const string file = "item_table_array.bin"; | |
| var dest = Path.Combine(DEST_MASTER, "world/data/ui/item_machine/item_table/"); | |
| var path = Path.Combine(PATH_MASTER, "world/data/ui/item_machine/item_table/"); | |
| var obj = FlatBufferConverter.DeserializeFrom<ItemMachineItemTableArray>(Path.Combine(path, file)); | |
| var ordered = obj.Table.OrderByDescending(z => z.Param.Value.EmergePercent).ToList(); | |
| List<(ItemID Item, int Min, int Max, int Rate)> OldItems = []; | |
| List<(ItemID Item, int Min, int Max, int Rate)> NewItems = []; | |
| List<ItemID> Used = []; | |
| foreach (var item in ordered.Where(z => !GetIsTeraShard((ushort)z.Param.Value.ItemId))) | |
| { | |
| var t = item.Param.Value; | |
| OldItems.Add((t.ItemId, t.LotteryItemNumMin, t.LotteryItemNumMax, t.EmergePercent)); | |
| t.ItemId = GetRandomItem(); | |
| // don't allow duplicate items | |
| while (Used.Contains(t.ItemId)) | |
| t.ItemId = GetRandomItem(); | |
| // don't give excessive quantities | |
| if (ValuableItems.Contains((ushort)t.ItemId)) | |
| { | |
| t.LotteryItemNumMin = 1; | |
| t.LotteryItemNumMax = 1; | |
| } | |
| NewItems.Add((t.ItemId, t.LotteryItemNumMin, t.LotteryItemNumMax, t.EmergePercent)); | |
| Used.Add(t.ItemId); | |
| } | |
| if (GENERATE_SPOILER_LOG_FILE) | |
| { | |
| SpoilerLog.Add("== ITEM PRINTER ITEMS =="); | |
| SpoilerLog.Add("================================================================================================"); | |
| SpoilerLog.Add("| Rate | Old Item | New Item | Quantity |"); | |
| SpoilerLog.Add("================================================================================================"); | |
| var sep = GetRangeSeparator(LANGUAGE); | |
| for (int i = 0; i < NewItems.Count; i++) | |
| { | |
| var oldItem = OldItems[i]; | |
| var newItem = NewItems[i]; | |
| var quantity = newItem.Min == newItem.Max ? newItem.Min.ToString() : $"{newItem.Min}{sep}{newItem.Max}"; | |
| float rate = newItem.Rate / 100.0f; | |
| SpoilerLog.Add($"| {rate:00.00}% | {StringsItem[(ushort)oldItem.Item], -32} | {StringsItem[(ushort)newItem.Item], -32} | {quantity, -13} |"); | |
| } | |
| SpoilerLog.Add("================================================================================================"); | |
| SpoilerLog.Add(""); | |
| } | |
| if (!Directory.Exists(dest)) | |
| Directory.CreateDirectory(dest); | |
| var data = FlatBufferConverter.SerializeFrom(obj); | |
| File.WriteAllBytes(Path.Combine(dest, file), data); | |
| } | |
| private static void RandomizeTeraRaidItemDrops() | |
| { | |
| const string fileB = "raid_lottery_reward_item_array.bin"; | |
| var destB = Path.Combine(DEST_MASTER, "world/data/raid/raid_lottery_reward_item/"); | |
| var pathB = Path.Combine(PATH_MASTER, "world/data/raid/raid_lottery_reward_item/"); | |
| var objB = FlatBufferConverter.DeserializeFrom<RaidLotteryRewardItemArray>(Path.Combine(pathB, fileB)); | |
| const string fileF = "raid_fixed_reward_item_array.bin"; | |
| var destF = Path.Combine(DEST_MASTER, "world/data/raid/raid_fixed_reward_item/"); | |
| var pathF = Path.Combine(PATH_MASTER, "world/data/raid/raid_fixed_reward_item/"); | |
| var objF = FlatBufferConverter.DeserializeFrom<RaidFixedRewardItemArray>(Path.Combine(pathF, fileF)); | |
| List<(ItemID Item, int Quantity, ulong TableID, float Rate)> OldItemsBonus = []; | |
| List<(ItemID Item, int Quantity, ulong TableID, float Rate)> NewItemsBonus = []; | |
| List<(ItemID Item, int Quantity, ulong TableID)> OldItemsFixed = []; | |
| List<(ItemID Item, int Quantity, ulong TableID)> NewItemsFixed = []; | |
| const int COUNT_BONUS = RaidLotteryRewardItem.RewardItemCount; | |
| const int COUNT_FIXED = RaidFixedRewardItem.Count; | |
| if (RANDOMIZE_TERA_RAID_BONUS_ITEM_DROPS) | |
| { | |
| "Randomizing Tera Raid Battle bonus item drops...".Dump(); | |
| foreach (var t in objB.Table.Where(z => z.TableName != 0xAFC4D6C05ABEF19E)) | |
| { | |
| float totalRate = 0; | |
| for (int i = 0; i < COUNT_BONUS; i++) | |
| totalRate += t.GetRewardItem(i).Rate; | |
| for (int i = 0; i < COUNT_BONUS; i++) | |
| { | |
| var drop = t.GetRewardItem(i); | |
| if (drop.ItemID == 0) | |
| continue; | |
| float rate = (float)(Math.Round((drop.Rate / totalRate) * 100.0f, 2)); | |
| OldItemsBonus.Add((drop.ItemID, drop.Num, t.TableName, rate)); | |
| drop.ItemID = GetRandomItem(); | |
| // don't give excessive quantities, but disregard Exp. Candy L/XL | |
| if (!GetIsExperienceCandy((ushort)drop.ItemID) && ValuableItems.Contains((ushort)drop.ItemID)) | |
| drop.Num = 1; | |
| // account for future quantity increases | |
| var adjusted = drop.ItemID switch | |
| { | |
| _ when GetIsExperienceCandy((ushort)drop.ItemID) => drop.Num *= 2, | |
| _ when GetIsTeraShard((ushort)drop.ItemID) => drop.Num *= 5, | |
| _ => drop.Num, | |
| }; | |
| NewItemsBonus.Add((drop.ItemID, adjusted, t.TableName, rate)); | |
| } | |
| } | |
| } | |
| if (RANDOMIZE_TERA_RAID_FIXED_ITEM_DROPS) | |
| { | |
| "Randomizing Tera Raid Battle fixed item drops...".Dump(); | |
| foreach (var t in objF.Table.Where(z => z.TableName != 0xAFC4D6C05ABEF19E)) | |
| { | |
| for (int i = 0; i < COUNT_FIXED; i++) | |
| { | |
| var drop = t.GetReward(i); | |
| if (drop.ItemID == 0) | |
| continue; | |
| OldItemsFixed.Add((drop.ItemID, drop.Num, t.TableName)); | |
| drop.ItemID = GetRandomItem(); | |
| // don't give excessive quantities, but disregard Exp. Candy L/XL | |
| if (!GetIsExperienceCandy((ushort)drop.ItemID) && ValuableItems.Contains((ushort)drop.ItemID)) | |
| drop.Num = 1; | |
| // account for future quantity increases | |
| var adjusted = drop.ItemID switch | |
| { | |
| _ when GetIsExperienceCandy((ushort)drop.ItemID) => drop.Num *= 2, | |
| _ when GetIsTeraShard((ushort)drop.ItemID) => drop.Num *= 5, | |
| _ => drop.Num, | |
| }; | |
| NewItemsFixed.Add((drop.ItemID, adjusted, t.TableName)); | |
| } | |
| } | |
| } | |
| foreach (var t in objB.Table) | |
| { | |
| for (int i = 0; i < COUNT_BONUS; i++) | |
| { | |
| var drop = t.GetRewardItem(i); | |
| if (GetIsExperienceCandy((ushort)drop.ItemID)) | |
| drop.Num *= 2; | |
| if (GetIsTeraShard((ushort)drop.ItemID) || drop.Category == RaidRewardItemCategoryType.GEM) | |
| drop.Num *= 5; | |
| } | |
| } | |
| foreach (var t in objF.Table) | |
| { | |
| for (int i = 0; i < COUNT_FIXED; i++) | |
| { | |
| var drop = t.GetReward(i); | |
| if (GetIsExperienceCandy((ushort)drop.ItemID)) | |
| drop.Num *= 2; | |
| if (GetIsTeraShard((ushort)drop.ItemID) || drop.Category == RaidRewardItemCategoryType.GEM) | |
| drop.Num *= 5; | |
| drop.SubjectType = RaidRewardItemSubjectType.ALL; | |
| } | |
| } | |
| if (RANDOMIZE_TERA_RAID_BONUS_ITEM_DROPS && GENERATE_SPOILER_LOG_FILE) | |
| { | |
| SpoilerLog.Add("== TERA RAID BATTLES: BONUS ITEM DROPS =="); | |
| SpoilerLog.Add("======================================================================================================="); | |
| SpoilerLog.Add("| Item Table | Rate | Old Item | New Item |"); | |
| SpoilerLog.Add("======================================================================================================="); | |
| for (int i = 0; i < NewItemsBonus.Count; i++) | |
| { | |
| var oldItem = OldItemsBonus[i]; | |
| var newItem = NewItemsBonus[i]; | |
| var rate = NewItemsBonus[i].Rate; | |
| var tableID = NewItemsBonus[i].TableID; | |
| var tierDisplay = GetItemTableTierDisplayString(tableID); | |
| var oldItemDisplay = $"{StringsItem[(ushort)oldItem.Item]} (×{oldItem.Quantity})"; | |
| var newItemDisplay = $"{StringsItem[(ushort)newItem.Item]} (×{newItem.Quantity})"; | |
| SpoilerLog.Add($"| {tierDisplay, -20} | {rate:00.00}% | {oldItemDisplay, -32} | {newItemDisplay, -32} |"); | |
| } | |
| SpoilerLog.Add("======================================================================================================="); | |
| SpoilerLog.Add(""); | |
| } | |
| if (RANDOMIZE_TERA_RAID_FIXED_ITEM_DROPS && GENERATE_SPOILER_LOG_FILE) | |
| { | |
| SpoilerLog.Add("== TERA RAID BATTLES: FIXED ITEM DROPS =="); | |
| SpoilerLog.Add("=============================================================================================="); | |
| SpoilerLog.Add("| Item Table | Old Item | New Item |"); | |
| SpoilerLog.Add("=============================================================================================="); | |
| for (int i = 0; i < NewItemsFixed.Count; i++) | |
| { | |
| var oldItem = OldItemsFixed[i]; | |
| var newItem = NewItemsFixed[i]; | |
| var tableID = NewItemsFixed[i].TableID; | |
| var tierDisplay = GetItemTableTierDisplayString(tableID); | |
| var oldItemDisplay = $"{StringsItem[(ushort)oldItem.Item]} (×{oldItem.Quantity})"; | |
| var newItemDisplay = $"{StringsItem[(ushort)newItem.Item]} (×{newItem.Quantity})"; | |
| SpoilerLog.Add($"| {tierDisplay, -20} | {oldItemDisplay, -32} | {newItemDisplay, -32} |"); | |
| } | |
| SpoilerLog.Add("=============================================================================================="); | |
| SpoilerLog.Add(""); | |
| } | |
| if (!Directory.Exists(destB)) Directory.CreateDirectory(destB); | |
| if (!Directory.Exists(destF)) Directory.CreateDirectory(destF); | |
| var dataB = FlatBufferConverter.SerializeFrom(objB); | |
| var dataF = FlatBufferConverter.SerializeFrom(objF); | |
| File.WriteAllBytes(Path.Combine(destB, fileB), dataB); | |
| File.WriteAllBytes(Path.Combine(destF, fileF), dataF); | |
| } | |
| private static void RandomizeLeagueRepRewards() | |
| { | |
| "Randomizing Pokémon League rep rewards...".Dump(); | |
| const string file = "world_npc_0.trscn"; | |
| var dest = Path.Combine(DEST_MASTER, "world/scene/parts/field/streaming_event/world_npc_/"); | |
| var path = Path.Combine(PATH_MASTER, "world/scene/parts/field/streaming_event/world_npc_/"); | |
| var obj = FlatBufferConverter.DeserializeFrom<TrinitySceneObjectTemplate>(Path.Combine(path, file)); | |
| List<(ulong Item, int Quantity)> OldItems = []; | |
| List<(ulong Item, int Quantity)> NewItems = []; | |
| foreach (var o in obj.Objects.Where(z => z.Type == "trinity_ObjectTemplate")) | |
| { | |
| foreach (var s in o.SubObjects.Where(z => z.Type == "trinity_SceneObject")) | |
| { | |
| foreach (var t in s.SubObjects.Where(z => z.Type == "trinity_PropertySheet")) | |
| { | |
| var tr = FlatBufferConverter.DeserializeFrom<TrinityPropertySheet>(t.Data); | |
| foreach (var p in tr.Properties) | |
| { | |
| bool modify = false; | |
| foreach (var i in p.Fields.Where(z => z.Name == "ItemID")) | |
| { | |
| var reward = i.Data.Item7; | |
| if (reward.Value != 0) | |
| { | |
| OldItems.Add((reward.Value, 1)); | |
| reward.Value = GetIsTechnicalMachine(reward.Value) ? (uint)GetRandomTM() : (uint)GetRandomItem(); | |
| reward.Enum = ((ItemID)reward.Value).ToString(); | |
| modify = GetIsTeraShard((ushort)reward.Value); | |
| NewItems.Add((reward.Value, modify ? 50 : 1)); | |
| } | |
| } | |
| if (modify) | |
| { | |
| foreach (var i in p.Fields.Where(z => z.Name == "ItemNum")) | |
| { | |
| var quantity = i.Data.Item1; | |
| if (quantity.Value != 0) | |
| quantity.Value = 50; | |
| } | |
| } | |
| t.Data = FlatBufferConverter.SerializeFrom(tr); | |
| } | |
| } | |
| } | |
| } | |
| if (GENERATE_SPOILER_LOG_FILE) | |
| { | |
| SpoilerLog.Add("== POKÉMON LEAGUE REP REWARDS =="); | |
| SpoilerLog.Add("==================================================================================================================================="); | |
| SpoilerLog.Add("| Location | Old Item | New Item |"); | |
| SpoilerLog.Add("==================================================================================================================================="); | |
| var locations = new[] { 012, 016, 050, 019, 026, 025, 020, 022, 023, 043, 034, 029, 031, 036, 057, 042, 044, 038, 039 }; | |
| for (int i = 0; i < NewItems.Count; i++) | |
| { | |
| var oldItem = OldItems[i]; | |
| var newItem = NewItems[i]; | |
| var oldDisplay = $"{StringsItem[oldItem.Item]} (×{oldItem.Quantity})"; | |
| var newDisplay = $"{StringsItem[newItem.Item]} (×{newItem.Quantity})"; | |
| var area = StringsMapPoint[locations[i]]; | |
| if (GetIsTechnicalMachine(newItem.Item)) | |
| { | |
| oldDisplay = GetTechnicalMachineDisplay(oldItem.Item, true); | |
| newDisplay = GetTechnicalMachineDisplay(newItem.Item, false); | |
| } | |
| SpoilerLog.Add($"| {area, -39} | {oldDisplay, -41} | {newDisplay, -41} |"); | |
| } | |
| SpoilerLog.Add("==================================================================================================================================="); | |
| } | |
| if (!Directory.Exists(dest)) | |
| Directory.CreateDirectory(dest); | |
| var data = FlatBufferConverter.SerializeFrom(obj); | |
| File.WriteAllBytes(Path.Combine(dest, file), data); // Scarlet | |
| File.WriteAllBytes(Path.Combine(dest, file.Replace("_0.", "_1.")), data); // Violet | |
| } | |
| private static void RandomizeRestaurantMealPowers() | |
| { | |
| if (!RANDOMIZE_RESTAURANT_FOOD_MEAL_POWERS) | |
| return; | |
| "Randomizing restaurant food Meal Power bonuses...".Dump(); | |
| const string file = "restaurant_bufdata_array.bin"; | |
| var dest = Path.Combine(DEST_MASTER, "world/data/ui/shop/shop_restaurant/restaurant_bufdata/"); | |
| var path = Path.Combine(PATH_MASTER, "world/data/ui/shop/shop_restaurant/restaurant_bufdata/"); | |
| var obj = FlatBufferConverter.DeserializeFrom<BufDataArray>(Path.Combine(path, file)); | |
| foreach (var t in obj.Table) | |
| { | |
| var powers = GetRandomMealPowers(); | |
| t.Buftype01 = powers[0]; | |
| t.Buflevel01 = GetRandomMealPowerLevel(); | |
| t.BufPoketype01 = GetRandomMealPowerType(t.Buftype01); | |
| t.Buftype02 = powers[1]; | |
| t.Buflevel02 = GetRandomMealPowerLevel(); | |
| t.BufPoketype02 = GetRandomMealPowerType(t.Buftype02); | |
| t.Buftype03 = powers[2]; | |
| t.Buflevel03 = GetRandomMealPowerLevel(); | |
| t.BufPoketype03 = GetRandomMealPowerType(t.Buftype03); | |
| } | |
| if (!Directory.Exists(dest)) | |
| Directory.CreateDirectory(dest); | |
| var data = FlatBufferConverter.SerializeFrom(obj); | |
| File.WriteAllBytes(Path.Combine(dest, file), data); | |
| } | |
| private static List<FoodSkillType> GetRandomMealPowers() | |
| { | |
| var powers = Enumerable.Range(1, 10).Select(z => (FoodSkillType)z).ToList(); | |
| ShuffleData(powers); | |
| return powers[..3]; | |
| } | |
| private static int GetRandomMealPowerLevel() => GetRandom(100) switch | |
| { | |
| <= 99 and >= 55 => 1, // 45% | |
| <= 54 and >= 20 => 2, // 35% | |
| _ => 3, // 20% | |
| }; | |
| private static MoveType GetRandomMealPowerType(FoodSkillType type) | |
| { | |
| // Egg Power: unrelated to type | |
| if (type == FoodSkillType.EGG) | |
| return MoveType.Null; | |
| // all Meal Powers can be "All Types" except for Encounter Power | |
| var max = type == FoodSkillType.ENCOUNT ? MAX_TYPE_ID + 1 : MAX_TYPE_ID + 2; | |
| return (MoveType)GetRandom(max); | |
| } | |
| private static string GetTechnicalMachineIcon(ushort move) => GetMoveType(move) switch | |
| { | |
| (byte)TypeIndex.Normal => "item_0328", | |
| (byte)TypeIndex.Fighting => "item_0339", | |
| (byte)TypeIndex.Flying => "item_0341", | |
| (byte)TypeIndex.Poison => "item_0340", | |
| (byte)TypeIndex.Ground => "item_0332", | |
| (byte)TypeIndex.Rock => "item_0363", | |
| (byte)TypeIndex.Bug => "item_0342", | |
| (byte)TypeIndex.Ghost => "item_0344", | |
| (byte)TypeIndex.Steel => "item_0358", | |
| (byte)TypeIndex.Fire => "item_0335", | |
| (byte)TypeIndex.Water => "item_0338", | |
| (byte)TypeIndex.Grass => "item_0347", | |
| (byte)TypeIndex.Electric => "item_0336", | |
| (byte)TypeIndex.Psychic => "item_0331", | |
| (byte)TypeIndex.Ice => "item_0337", | |
| (byte)TypeIndex.Dragon => "item_0371", | |
| (byte)TypeIndex.Dark => "item_0330", | |
| (byte)TypeIndex.Fairy => "item_0329", | |
| _ => throw new ArgumentException(nameof(move)), | |
| }; | |
| private static void UpdateShopLineups() | |
| { | |
| "Updating shop inventories...".Dump(); | |
| const string file = "friendlyshop_lineup_data_array.bin"; | |
| var dest = Path.Combine(DEST_MASTER, "world/data/ui/shop/friendlyshop/friendlyshop_lineup_data/"); | |
| var path = Path.Combine(PATH_MASTER, "world/data/ui/shop/friendlyshop/friendlyshop_lineup_data/"); | |
| var obj = FlatBufferConverter.DeserializeFrom<LineupDataArray>(Path.Combine(path, file)); | |
| var evolutionItems = new[] | |
| { | |
| 2354, 0080, 0081, 0107, 0108, 0109, | |
| 0110, 0235, 0252, 0321, 0322, 0323, | |
| 0324, 0325, 0537, 0849, 1109, 1110, | |
| 1111, 1112, 1113, 1114, 1115, 1253, | |
| 1254, 1582, 1592, 2351, 2352, 1861, | |
| 2344, 2402, 2403, 2404, 2482, 2353, | |
| }; | |
| var customSortTable = new[] | |
| { | |
| 0795, 0853, 0854, 0855, 0856, 0228, 0218, | |
| 0229, 0294, 0289, 0290, 0291, 0292, 0293, | |
| 0082, 0083, 0085, 0084, 0080, 0081, 0107, | |
| 0108, 0109, 0110, 0849, 2354, 0235, 0252, | |
| 0321, 0322, 0323, 0324, 0537, 0325, 1117, | |
| 1116, 2402, 1253, 1254, 1109, 1110, 1111, | |
| 1112, 1113, 1114, 1115, 1582, 1592, 2351, | |
| 2352, 2344, 1861, 2403, 2404, 2482, 2353, | |
| }; | |
| var battleItems = obj.Table.Where(z => z.Lineupid.EndsWith("_lineup1")).ToList(); | |
| obj.Table = obj.Table.Where(z => !z.Lineupid.EndsWith("_lineup1") && (ushort)z.Item is not (2351 or 2352 or 2353 or 2354)).ToList(); | |
| // reduce post-game exclusive items to after receiving 8 Gym Badges | |
| foreach (var t in obj.Table.Where(z => z.GymBadgeNum == 9)) | |
| { | |
| t.ItemCondvalue = "BADGE8"; | |
| t.GymBadgeNum = 8; | |
| } | |
| // reduce all picnic items to not require any Gym Badges | |
| foreach (var t in obj.Table.Where(z => GetIsPicnicItem((ushort)z.Item))) | |
| { | |
| t.ItemCondvalue = string.Empty; | |
| t.GymBadgeNum = 0; | |
| } | |
| // add evolution items to all Delibird Presents shops | |
| for (int i = 0; i <= 2; i++) | |
| { | |
| foreach (var item in evolutionItems) | |
| { | |
| var entry = new LineupData() | |
| { | |
| ItemCondkind = CondEnum.GYMBADGENUM, | |
| ItemCondvalue = "BADGE3", | |
| GymBadgeNum = 3, | |
| Lineupid = $"shop_delibird_{i:00}_lineup2", | |
| Item = (ItemID)item, | |
| Sortnum = 0, | |
| }; | |
| obj.Table.Add(entry); | |
| } | |
| } | |
| // tweak sort for Delibird Presents (General Goods) | |
| foreach (var t in obj.Table.Where(z => z.Lineupid.EndsWith("_lineup2"))) | |
| t.Sortnum = customSortTable.IndexOf((ushort)t.Item); | |
| // standardize Battle Item inventories across all branches | |
| for (int i = 0; i <= 2; i++) | |
| { | |
| var id = $"shop_delibird_{i:00}_lineup1"; | |
| var extra = new[] { 0327, 2401 }; | |
| var sort = 1; | |
| // vanilla items | |
| foreach (var t in battleItems) | |
| { | |
| var entry = new LineupData() | |
| { | |
| ItemCondkind = t.ItemCondkind, | |
| ItemCondvalue = t.ItemCondvalue, | |
| GymBadgeNum = t.GymBadgeNum, | |
| Lineupid = id, | |
| Item = t.Item, | |
| Sortnum = sort, | |
| }; | |
| obj.Table.Add(entry); | |
| sort++; | |
| } | |
| // Razor Fang, Fairy Feather | |
| foreach (var t in extra) | |
| { | |
| var entry = new LineupData() | |
| { | |
| ItemCondkind = CondEnum.NONE, | |
| ItemCondvalue = string.Empty, | |
| GymBadgeNum = 0, | |
| Lineupid = id, | |
| Item = (ItemID)t, | |
| Sortnum = sort, | |
| }; | |
| obj.Table.Add(entry); | |
| sort++; | |
| } | |
| } | |
| // add TM Materials to Poké Marts | |
| if (!RANDOMIZE_TM_RECIPES) | |
| { | |
| var ctr = 26; | |
| var materials = GetSortedMaterialList(); | |
| foreach (var t in materials) | |
| { | |
| var material = new LineupData() | |
| { | |
| Lineupid = "shop_00_lineup", | |
| Sortnum = ctr, | |
| Item = t, | |
| ItemCondkind = CondEnum.NONE, | |
| ItemCondvalue = string.Empty, | |
| GymBadgeNum = 0, | |
| }; | |
| obj.Table.Add(material); | |
| ctr++; | |
| } | |
| } | |
| // add Mythical Pecha Berry to Peachy's | |
| foreach (var t in obj.Table.Where(z => z.Lineupid == "shop_syouten_lineup")) | |
| t.Sortnum++; | |
| var mythicalPechaBerry = new LineupData() | |
| { | |
| ItemCondkind = CondEnum.SYSTEM_FLAG, | |
| ItemCondvalue = "FSYS_SCENARIO_GAME_CLEAR_SU2", | |
| GymBadgeNum = 8, | |
| Lineupid = "shop_syouten_lineup", | |
| Item = ItemID.ITEMID_GOKUZYOUMOMON, | |
| Sortnum = 1, | |
| }; | |
| obj.Table.Add(mythicalPechaBerry); | |
| obj.Table = obj.Table.OrderBy(z => z.Lineupid).ThenBy(z => z.Sortnum).ToList(); | |
| if (RANDOMIZE_SANDWICH_SHOP_INVENTORIES) | |
| { | |
| var items = GetRandomItemList(); | |
| var ctr = 0; | |
| foreach (var t in obj.Table.Where(z => GetIsPicnicItem((ushort)z.Item))) | |
| { | |
| t.Item = items[ctr]; | |
| ctr++; | |
| } | |
| } | |
| if (!Directory.Exists(dest)) | |
| Directory.CreateDirectory(dest); | |
| var data = FlatBufferConverter.SerializeFrom(obj); | |
| File.WriteAllBytes(Path.Combine(dest, file), data); | |
| } | |
| private static void UpdateItemData() | |
| { | |
| "Updating item properties...".Dump(); | |
| const string file = "itemdata_array.bin"; | |
| var dest = Path.Combine(DEST_MASTER, "world/data/item/itemdata/"); | |
| var source = File.Exists(Path.Combine(dest, file)) ? Path.Combine(dest, file) : Path.Combine(PATH_MASTER, "world/data/item/itemdata/", file); | |
| var obj = FlatBufferConverter.DeserializeFrom<ItemDataArray>(source); | |
| var tradeEvolutionItems = new[] { 0221, 0233, 0235, 0252, 0321, 0322, 0323, 0324, 0325, 0537 }; | |
| var customSortTable = new[] | |
| { | |
| 0050, 1124, 1125, 1126, 1127, 1128, 0795, 0796, 0645, 1606, 0051, 0053, 0045, 0046, 0047, 0049, 0052, 0048, 0565, 0566, 0567, 0568, 0569, 0570, 2411, | |
| 2412, 2413, 2414, 2415, 2416, 2479, 1231, 1232, 1233, 1234, 1235, 1236, 1237, 1238, 1239, 1240, 1241, 1242, 1243, 1244, 1245, 1246, 1247, 1248, 1249, | |
| 1250, 1251, 0213, 0214, 0217, 0218, 0219, 0220, 0221, 0222, 0223, 0224, 0228, 0229, 0230, 0231, 0232, 0233, 0234, 0236, 0237, 0238, 0239, 0240, 0241, | |
| 0242, 0243, 0244, 0245, 0246, 0247, 0248, 0249, 0250, 0251, 0253, 0265, 0266, 0267, 0268, 0269, 0270, 0271, 0272, 0273, 0275, 0276, 0277, 0278, 0279, | |
| 0280, 0281, 0282, 0283, 0284, 0285, 0286, 0287, 0288, 0289, 0290, 0291, 0292, 0293, 0294, 0295, 0296, 0297, 0326, 0327, 0538, 0539, 0540, 0541, 0542, | |
| 0543, 0544, 0545, 0546, 0547, 0564, 0639, 0640, 0648, 0649, 0650, 0846, 0879, 0880, 0881, 0882, 0883, 0884, 1118, 1119, 1120, 1121, 1122, 1123, 0853, | |
| 0854, 0855, 0856, 1880, 1881, 1882, 1883, 1884, 1885, 1886, 2345, 2401, 0082, 0083, 0085, 0084, 0080, 0081, 0107, 0108, 0109, 0110, 0849, 2354, 0235, | |
| 0252, 0321, 0322, 0323, 0324, 0537, 0325, 1117, 1116, 2402, 1253, 1254, 1109, 1110, 1111, 1112, 1113, 1114, 1115, 1581, 1582, 1592, 2351, 2352, 2344, | |
| 1861, 2403, 2404, 2482, 2353, 0225, 0135, 1777, 0136, 1778, 0112, 1779, 0298, 0299, 0300, 0301, 0302, 0303, 0304, 0305, 0306, 0307, 0308, 0309, 0310, | |
| 0311, 0312, 0313, 0644, 1103, 1104, 2407, 2408, 2406, 1862, 1863, 1864, 1865, 1866, 1867, 1868, 1869, 1870, 1871, 1872, 1873, 1874, 1875, 1876, 1877, | |
| 1878, 1879, 2549, | |
| }; | |
| // make trade evolution items usable | |
| foreach (var t in obj.Table.Where(z => tradeEvolutionItems.Contains(z.Id))) | |
| { | |
| t.FieldFunctionType = FieldFunctionType.FIELDFUNC_EVOLUTION; | |
| t.WorkType = WorkType.WORKTYPE_EffectPokemon; | |
| t.WorkEvolutional = 1; | |
| } | |
| obj.Table.FirstOrDefault(z => z.Id == 2550).Price = 100000; // adjust Mythical Pecha Berry price | |
| // the game sanitizes invalid items from the bag on boot, so we need to replace existing, valid items to insert our custom items | |
| // this removes Steel Bottle R/Y/B and Silver Bottle, so nothing of major value was lost | |
| obj.Table.Remove(obj.Table.FirstOrDefault(z => z.Id == 2351)); | |
| obj.Table.Remove(obj.Table.FirstOrDefault(z => z.Id == 2352)); | |
| obj.Table.Remove(obj.Table.FirstOrDefault(z => z.Id == 2353)); | |
| obj.Table.Remove(obj.Table.FirstOrDefault(z => z.Id == 2354)); | |
| // add the custom items! | |
| for (int i = 2351; i <= 2354; i++) | |
| { | |
| var entry = new ItemData() | |
| { | |
| FieldFunctionType = FieldFunctionType.FIELDFUNC_EVOLUTION, | |
| FieldPocket = FieldPocket.FPOCKET_OTHER, | |
| IconName = $"item_{i}", | |
| Id = i, | |
| ItemType = ItemType.ITEMTYPE_EQUIP, | |
| NaturalGiftType = 18, | |
| Price = 3000, | |
| SetToPoke = true, | |
| SlotMaxNum = 999, | |
| SortNum = 0, | |
| ThrowPower = 30, | |
| WorkEvolutional = 1, | |
| WorkType = WorkType.WORKTYPE_EffectPokemon, | |
| }; | |
| obj.Table.Add(entry); | |
| } | |
| if (BALANCE_SHOP_ITEM_PRICES) | |
| BalanceShopItemPrices(obj.Table); | |
| // apply custom sort indices | |
| foreach (var t in obj.Table.Where(z => customSortTable.Contains(z.Id))) | |
| t.SortNum = customSortTable.IndexOf(t.Id) + 1; | |
| if (!Directory.Exists(dest)) | |
| Directory.CreateDirectory(dest); | |
| var data = FlatBufferConverter.SerializeFrom(obj); | |
| File.WriteAllBytes(Path.Combine(dest, file), data); | |
| } | |
| private static void UpdateBlueberryQuests() | |
| { | |
| "Updating Snacksworth BBQ requirements...".Dump(); | |
| const string file = "release_poke_array.bin"; | |
| var dest = Path.Combine(DEST_MASTER, "world/data/event/s2_sub_012_legend_poke/release_poke/"); | |
| var path = Path.Combine(PATH_MASTER, "world/data/event/s2_sub_012_legend_poke/release_poke/"); | |
| var obj = FlatBufferConverter.DeserializeFrom<ReleasePokeTableArray>(Path.Combine(path, file)); | |
| foreach (var t in obj.Table) | |
| { | |
| t.MissionA = t.MissionB = true; | |
| t.TeamMissionA = t.TeamMissionB = false; | |
| } | |
| if (!Directory.Exists(dest)) | |
| Directory.CreateDirectory(dest); | |
| var data = FlatBufferConverter.SerializeFrom(obj); | |
| File.WriteAllBytes(Path.Combine(dest, file), data); | |
| } | |
| private static void UpdateBoutiqueAndSalonLineups() | |
| { | |
| "Updating boutique and salon lineups...".Dump(); | |
| const string file = "dressup_shop_data_array.bin"; | |
| var dest = Path.Combine(DEST_MASTER, "world/data/ui/shop/dressup_shop/dressup_shop_data/"); | |
| var path = Path.Combine(PATH_MASTER, "world/data/ui/shop/dressup_shop/dressup_shop_data/"); | |
| var obj = FlatBufferConverter.DeserializeFrom<DressupShopDataArray>(Path.Combine(path, file)); | |
| foreach (var t in obj.Table) | |
| t.UnlockType = DressupHairsalonUnlockType.NONE; | |
| if (!Directory.Exists(dest)) | |
| Directory.CreateDirectory(dest); | |
| var data = FlatBufferConverter.SerializeFrom(obj); | |
| File.WriteAllBytes(Path.Combine(dest, file), data); | |
| } | |
| private static void UpdateDropItemTable() | |
| { | |
| "Updating Pokémon drop item table...".Dump(); | |
| const string file = "dropitemdata_array.bin"; | |
| var dest = Path.Combine(DEST_MASTER, "world/data/item/dropitemdata/"); | |
| var path = Path.Combine(PATH_MASTER, "world/data/item/dropitemdata/"); | |
| var obj = FlatBufferConverter.DeserializeFrom<DropItemDataArray>(Path.Combine(path, file)); | |
| obj.Table.FirstOrDefault(z => z.DevId == DevID.DEV_KOIN1).Item1.Num = 05; // Gimmighoul | |
| obj.Table.FirstOrDefault(z => z.DevId == DevID.DEV_KOIN2).Item1.Num = 30; // Gholdengo | |
| if (!Directory.Exists(dest)) | |
| Directory.CreateDirectory(dest); | |
| var data = FlatBufferConverter.SerializeFrom(obj); | |
| File.WriteAllBytes(Path.Combine(dest, file), data); | |
| } | |
| private static void UpdateGlimmeringCharmTable() | |
| { | |
| "Updating Glimmering Charm reward table...".Dump(); | |
| const string file = "data.bin"; | |
| var dest = Path.Combine(DEST_MASTER, "world/data/raid/raid_gem_item_reward_boost/"); | |
| var path = Path.Combine(PATH_MASTER, "world/data/raid/raid_gem_item_reward_boost/"); | |
| var obj = FlatBufferConverter.DeserializeFrom<RaidGemItemRewardBoost>(Path.Combine(path, file)); | |
| obj.Difficulty1 = 5; | |
| obj.Difficulty2 = 10; | |
| obj.Difficulty3 = 15; | |
| obj.Difficulty4 = 25; | |
| obj.Difficulty5 = 35; | |
| obj.Difficulty6 = 50; | |
| if (!Directory.Exists(dest)) | |
| Directory.CreateDirectory(dest); | |
| var data = FlatBufferConverter.SerializeFrom(obj); | |
| File.WriteAllBytes(Path.Combine(dest, file), data); | |
| } | |
| private static void UpdatePaldeaPokedex() | |
| { | |
| "Updating Pokédex data...".Dump(); | |
| const string dir = "demo/appli/pokedex/zk0000_00/"; // 1024x1024 artwork | |
| const string dirP = "appli/tex/pokedex_capture_thum"; // 204x238 book cover, Paldea | |
| const string dirK = "_dlc1"; // 204x238 book cover, Kitakami | |
| const string dirB = "_dlc2"; // 204x238 book cover, Blueberry | |
| byte GetForm(ushort species) => species is 0128 or 0194 or 0901 ? (byte)1 : (byte)0; | |
| // Pokédex images (1024x1024) | |
| foreach (var t in PersonalInfoEdited.Table.Where(z => z.IsPresentInGame && z.Info.Form == GetForm(z.Info.SpeciesInternal) && z.Dex == null)) | |
| { | |
| var dev = t.Info.SpeciesInternal; | |
| var thumb = $"pokedex_capture_thum_{dev:000}"; | |
| var target = Path.Combine(DEST_MASTER, dir, $"{thumb}.bntx"); | |
| var original = Path.Combine(PATH_MASTER, dir) + t switch | |
| { | |
| _ when t.KitakamiDex != 0 => $"{thumb}_01.bntx", // demo/appli/pokedex/zk0000_00/pokedex_capture_thum_xxx_01.bntx | |
| _ when t.BlueberryDex != 0 => $"{thumb}_02.bntx", // demo/appli/pokedex/zk0000_00/pokedex_capture_thum_xxx_02.bntx | |
| _ => "pokedex_capture_thum_001.bntx", // placeholder black square | |
| }; | |
| if (!Directory.Exists(Path.Combine(DEST_MASTER, dir))) | |
| Directory.CreateDirectory(Path.Combine(DEST_MASTER, dir)); | |
| if (!File.Exists(target)) | |
| File.Copy(original, target); | |
| } | |
| // Pokédex images (204x238) | |
| foreach (var t in PersonalInfoEdited.Table.Where(z => z.IsPresentInGame && z.Info.Form == GetForm(z.Info.SpeciesInternal) && z.Dex == null)) | |
| { | |
| var dev = t.Info.SpeciesInternal; | |
| var thumb = $"pokedex_capture_thum_{dev:000}"; | |
| var targetDir = Path.Combine(DEST_MASTER, dirP, thumb); | |
| var target = Path.Combine(targetDir, $"{thumb}.bntx"); | |
| var original = t switch | |
| { | |
| _ when t.KitakamiDex != 0 => Path.Combine(PATH_MASTER, dirP + dirK, thumb, $"{thumb}.bntx"), // appli/tex/pokedex_capture_thum_dlc1/pokedex_capture_thum_xxx/pokedex_capture_thum_xxx.bntx | |
| _ when t.BlueberryDex != 0 => Path.Combine(PATH_MASTER, dirP + dirB, thumb, $"{thumb}.bntx"), // appli/tex/pokedex_capture_thum_dlc2/pokedex_capture_thum_xxx/pokedex_capture_thum_xxx.bntx | |
| _ => Path.Combine(PATH_MASTER, dir, "pokedex_capture_thum_001.bntx"), // placeholder black square | |
| }; | |
| if (!Directory.Exists(targetDir)) | |
| Directory.CreateDirectory(targetDir); | |
| if (!File.Exists(target)) | |
| File.Copy(original, target); | |
| } | |
| // Pokédex indices | |
| foreach (var t in PersonalInfoEdited.Table.Where(z => z.IsPresentInGame)) | |
| t.Dex = new PersonalInfoDex { Index = t.Info.SpeciesNational, Group = 0 }; | |
| UpdatePersonalTable(); | |
| WipePokedexBlacklistData(); | |
| FillEmptyPokedexEntries(); | |
| AddExtraPokedexData(); | |
| } | |
| private static void WipePokedexBlacklistData() | |
| { | |
| const string file = "blacklist_array.bin"; | |
| var dest = Path.Combine(DEST_MASTER, "world/data/ui/pokedex/blacklist/"); | |
| var path = Path.Combine(PATH_MASTER, "world/data/ui/pokedex/blacklist/"); | |
| var obj = FlatBufferConverter.DeserializeFrom<BlacklistArray>(Path.Combine(path, file)); | |
| obj.Table.Clear(); | |
| for (int i = 1; i <= 4; i++) obj.Table.Add(new Blacklist { DevNo = 0998, FormNo = i }); // Koraidon | |
| for (int i = 1; i <= 4; i++) obj.Table.Add(new Blacklist { DevNo = 0999, FormNo = i }); // Miraidon | |
| if (!Directory.Exists(dest)) | |
| Directory.CreateDirectory(dest); | |
| var data = FlatBufferConverter.SerializeFrom(obj); | |
| File.WriteAllBytes(Path.Combine(dest, file), data); | |
| } | |
| private static void FillEmptyPokedexEntries() | |
| { | |
| const int AHTB_MAGIC = 0x42544841; // AHTB | |
| var missing = new[] | |
| { | |
| "ZKN_COMMENT_{0}_058_001", // Growlithe (Hisuian Form) | |
| "ZKN_COMMENT_{0}_059_001", // Arcanine (Hisuian Form) | |
| "ZKN_COMMENT_{0}_100_001", // Voltorb (Hisuian Form) | |
| "ZKN_COMMENT_{0}_101_001", // Electrode (Hisuian Form) | |
| "ZKN_COMMENT_{0}_157_001", // Typhlosion (Hisuian Form) | |
| "ZKN_COMMENT_{0}_215_001", // Sneasel (Hisuian Form) | |
| "ZKN_COMMENT_{0}_483_001", // Dialga (Origin Forme) | |
| "ZKN_COMMENT_{0}_484_001", // Palkia (Origin Forme) | |
| "ZKN_COMMENT_{0}_503_001", // Samurott (Hisuian Form) | |
| "ZKN_COMMENT_{0}_549_001", // Lilligant (Hisuian Form) | |
| "ZKN_COMMENT_{0}_570_001", // Zorua (Hisuian Form) | |
| "ZKN_COMMENT_{0}_571_001", // Zoroark (Hisuian Form) | |
| "ZKN_COMMENT_{0}_628_001", // Braviary (Hisuian Form) | |
| "ZKN_COMMENT_{0}_705_001", // Sliggoo (Hisuian Form) | |
| "ZKN_COMMENT_{0}_706_001", // Goodra (Hisuian Form) | |
| "ZKN_COMMENT_{0}_713_001", // Avalugg (Hisuian Form) | |
| "ZKN_COMMENT_{0}_724_001", // Decidueye (Hisuian Form) | |
| "ZKN_COMMENT_{0}_899_000", // Wyrdeer | |
| "ZKN_COMMENT_{0}_901_000", // Ursaluna | |
| "ZKN_COMMENT_{0}_903_000", // Sneasler | |
| "ZKN_COMMENT_{0}_905_000", // Enamorus (Incarnate Forme) | |
| "ZKN_COMMENT_{0}_905_001", // Enamorus (Therian Forme) | |
| "ZKN_COMMENT_{0}_1011_004", // Ogerpon (Teal Mask*) | |
| "ZKN_COMMENT_{0}_1011_005", // Ogerpon (Wellspring Mask*) | |
| "ZKN_COMMENT_{0}_1011_006", // Ogerpon (Hearthflame Mask*) | |
| "ZKN_COMMENT_{0}_1011_007", // Ogerpon (Cornerstone Mask*) | |
| }; | |
| for (int i = 0; i <= 1; i++) | |
| { | |
| var ver = i == 0 ? 'A' : 'B'; | |
| var oldEntries = GetEntryListAHTBCommon(LANGUAGE, $"zukan_comment_{ver}"); | |
| var newEntries = new List<AHTBEntry>(); | |
| foreach (var t in oldEntries.Where(z => !z.Name.Contains("msg_zukan"))) | |
| newEntries.Add(t); | |
| foreach (var t in missing) | |
| { | |
| var name = string.Format(t, ver); | |
| var entry = new AHTBEntry(FnvHash.HashFnv1a_64(name), name); | |
| newEntries.Add(entry); | |
| } | |
| newEntries.Add(oldEntries[oldEntries.Count - 1]); // always last: msg_zukan_comment_{ver}_max | |
| foreach (var lang in LanguagesToIterate) | |
| { | |
| var dest = Path.Combine(DEST_MASTER, $"message/dat/{lang}/common/zukan_comment_{ver}.tbl"); | |
| using var ms = new MemoryStream(); | |
| { | |
| using var bw = new BinaryWriter(File.Open(dest, FileMode.Create)); | |
| { | |
| bw.Write(AHTB_MAGIC); | |
| bw.Write((uint)newEntries.Count); | |
| foreach (var t in newEntries) | |
| { | |
| bw.Write((ulong)t.Hash); | |
| bw.Write((ushort)(t.Name.Length + 1)); | |
| bw.Write(Encoding.UTF8.GetBytes(t.Name)); | |
| bw.Write((byte)0); | |
| } | |
| } | |
| } | |
| } | |
| } | |
| foreach (var lang in LanguagesToIterate) | |
| { | |
| var dest = Path.Combine(DEST_MASTER, $"message/dat/{lang}/common/"); | |
| for (int j = 0; j <= 1; j++) | |
| { | |
| var ver = j == 0 ? 'A' : 'B'; | |
| var zkn = $"zukan_comment_{ver}"; | |
| var original = GetStringsCommon(lang, $"zukan_comment_{ver}"); | |
| var flags = GetStringFlagsCommon(lang, $"zukan_comment_{ver}"); | |
| var lines = new string[original.Length + missing.Length]; | |
| // add original entries | |
| for (int a = 0; a < original.Length; a++) | |
| lines[a] = original[a]; | |
| // add temporary empty entries | |
| for (int a = original.Length; a < lines.Length; a++) | |
| lines[a] = string.Empty; | |
| // add in new entries | |
| for (int k = 0; k < lines.Length - 4; k++) | |
| { | |
| if (string.IsNullOrWhiteSpace(lines[k]) || lines[k].StartsWith("[~ ")) | |
| lines[k] = GetPokedexEntry(k, lang, ver); | |
| } | |
| // add in new entries for Ogerpon's Terastallized States | |
| for (int k = lines.Length - 4; k < lines.Length; k++) | |
| { | |
| var source = k == 1463 ? 1007 : k - 30; | |
| lines[k] = lines[source]; | |
| } | |
| // clean up form entries | |
| lines[1228] = lines[0658]; // Greninja | |
| lines[1330] = lines[0744]; // Rockruff | |
| for (int l = 1230; l <= 1248; l++) lines[l] = lines[0664]; // Scatterbug | |
| for (int l = 1249; l <= 1267; l++) lines[l] = lines[0665]; // Spewpa | |
| if (!Directory.Exists(dest)) | |
| Directory.CreateDirectory(dest); | |
| var write = TextFile.GetBytes(lines, flags, TextConfig); | |
| File.WriteAllBytes(Path.Combine(dest, $"{zkn}.dat"), write); | |
| } | |
| } | |
| } | |
| private static void AddExtraPokedexData() | |
| { | |
| const int AHTB_MAGIC = 0x42544841; // AHTB | |
| (string File, string EntryFormat, string LastEntry) GetPokedexData(int type) => type switch | |
| { | |
| 0 => ("zkn_form", "ZKN_FORM_1011_{0:000}", "msg_zkn_form_max" ), | |
| 1 => ("zkn_type", "ZKN_TYPE_1011_{0:000}", "msg_zkn_type_max" ), | |
| 2 => ("zkn_height", "ZKN_HEIGHT_1011_{0:000}", "msg_zkn_height_max" ), | |
| 3 => ("zkn_weight", "ZKN_WEIGHT_1011_{0:000}", "msg_zkn_weight_max" ), | |
| _ => throw new ArgumentException(nameof(type)), | |
| }; | |
| for (int i = 0; i < 4; i++) | |
| { | |
| var dex = GetPokedexData(i); | |
| var file = dex.File; | |
| var entryFormat = dex.EntryFormat; | |
| var lastEntry = dex.LastEntry; | |
| var oldEntries = GetEntryListAHTBCommon(LANGUAGE, file); | |
| var newEntries = new List<AHTBEntry>(); | |
| foreach (var t in oldEntries.Where(z => z.Name != lastEntry)) | |
| newEntries.Add(t); | |
| var indexAlt = oldEntries.IndexOf(oldEntries.FirstOrDefault(z => z.Name == string.Format(entryFormat, 1))) - 1; | |
| var indexReg = file == "zkn_type" ? oldEntries.IndexOf(oldEntries.FirstOrDefault(z => z.Name == "ZKN_TYPE_1011")) | |
| : oldEntries.IndexOf(oldEntries.FirstOrDefault(z => z.Name == string.Format(entryFormat, 0))); | |
| for (int f = 4; f <= 7; f++) | |
| { | |
| var name = string.Format(entryFormat, f); | |
| var entry = new AHTBEntry(FnvHash.HashFnv1a_64(name), name); | |
| newEntries.Add(entry); | |
| } | |
| newEntries.Add(oldEntries[oldEntries.Count - 1]); | |
| foreach (var lang in LanguagesToIterate) | |
| { | |
| var dest = Path.Combine(DEST_MASTER, $"message/dat/{lang}/common/{file}.tbl"); | |
| using var ms = new MemoryStream(); | |
| { | |
| using var bw = new BinaryWriter(File.Open(dest, FileMode.Create)); | |
| { | |
| bw.Write(AHTB_MAGIC); | |
| bw.Write((uint)newEntries.Count); | |
| foreach (var t in newEntries) | |
| { | |
| bw.Write((ulong)t.Hash); | |
| bw.Write((ushort)(t.Name.Length + 1)); | |
| bw.Write(Encoding.UTF8.GetBytes(t.Name)); | |
| bw.Write((byte)0); | |
| } | |
| } | |
| } | |
| } | |
| foreach (var lang in LanguagesToIterate) | |
| { | |
| var dest = Path.Combine(DEST_MASTER, $"message/dat/{lang}/common/"); | |
| var original = GetStringsCommon(lang, file); | |
| var flags = GetStringFlagsCommon(lang, file); | |
| var lines = new string[original.Length + 4]; | |
| var ctr = 0; | |
| // add original entries | |
| for (int a = 0; a < original.Length; a++) | |
| lines[a] = original[a]; | |
| // add in new entries for Ogerpon's Terastallized States | |
| for (int a = original.Length; a < lines.Length; a++) | |
| { | |
| var source = ctr == 0 ? indexReg : indexAlt + ctr; | |
| lines[a] = lines[source]; | |
| ctr++; | |
| // something to differentiate in-game | |
| if (file == "zkn_form") | |
| lines[a] += '*'; | |
| } | |
| // revise extra form strings | |
| if (file == "zkn_form") | |
| { | |
| var line = lang switch | |
| { | |
| "JPN" or "JPN_KANJI" or "Simp_Chinese" => "{0}・{1}", | |
| "Korean" => "{0}/{1}", | |
| _ => "{0} ({1})", | |
| }; | |
| for (int l = 0; l < lines.Length; l++) | |
| { | |
| // silly workaround to display a blank line | |
| if (string.IsNullOrWhiteSpace(lines[l])) | |
| lines[l] = "\\n"; | |
| // Paldean Tauros | |
| if (l is >= 1089 and <= 1091) | |
| { | |
| var form = l - 1088; | |
| var formName = GetPaldeanTaurosSubForm(lang, form); | |
| lines[l] = string.Format(line, lines[l], formName); | |
| } | |
| // Greninja (Battle Bond) | |
| if (l == 1248) | |
| { | |
| var form = GetStringFromAHTBCommon(lang, "tokusei", FnvHash.HashFnv1a_64("TOKUSEI_210")); | |
| lines[l] = form; | |
| } | |
| // Rockruff (Own Tempo) | |
| if (l == 1355) | |
| { | |
| var form = GetStringFromAHTBCommon(lang, "tokusei", FnvHash.HashFnv1a_64("TOKUSEI_020")); | |
| lines[l] = form; | |
| } | |
| } | |
| } | |
| if (!Directory.Exists(dest)) | |
| Directory.CreateDirectory(dest); | |
| var write = TextFile.GetBytes(lines, flags, TextConfig); | |
| File.WriteAllBytes(Path.Combine(dest, $"{file}.dat"), write); | |
| } | |
| } | |
| } | |
| private static void UpdateItemSortTables() | |
| { | |
| "Updating item sort tables...".Dump(); | |
| foreach (var lang in LanguagesToIterate) | |
| { | |
| Dictionary<ushort, string> ItemDictionary = []; | |
| var items = GetStringsCommon(lang, "itemname"); | |
| for (ushort j = 1; j < items.Length; j++) | |
| ItemDictionary.Add(j, items[j]); | |
| ItemDictionary = ItemDictionary.OrderBy(z => z.Value).ToDictionary(); | |
| var dir = Path.Combine(DEST_MASTER, $"message/sort/{lang}/"); | |
| if (!Directory.Exists(dir)) | |
| Directory.CreateDirectory(dir); | |
| var dest = Path.Combine(dir, "item_sort_table.dat"); | |
| using var ms = new MemoryStream(); | |
| { | |
| using var bw = new BinaryWriter(File.Open(dest, FileMode.Create)); | |
| { | |
| foreach (var t in ItemDictionary) | |
| bw.Write(t.Key); | |
| } | |
| } | |
| } | |
| } | |
| private static void BalanceShopItemPrices(IList<ItemData> table) | |
| { | |
| bool IsItemToModify(int id) => id is 0012 or 1126 or 1127 or 1128 or 2411 or 2412 or 2413 or 2414 or 2415 or 2416 or 2479; | |
| foreach (var t in table.Where(z => z.Price == 0 || IsItemToModify(z.Id))) | |
| { | |
| t.Price = GetBalancedItemPrice(t.Id); | |
| t.BP = (int)(t.Price / 100.0f); | |
| } | |
| if (!RANDOMIZE_TM_RECIPES) | |
| { | |
| var materials = GetSortedMaterialList(); | |
| foreach (var t in materials) | |
| { | |
| var material = table.FirstOrDefault(z => z.Id == (ushort)t); | |
| material.Price = 1000; | |
| } | |
| } | |
| } | |
| private static void UpdateTrainerPopupText() | |
| { | |
| "Updating trainer popup text...".Dump(); | |
| var entries = GetEntryListAHTBCommon(LANGUAGE, "trmsg"); | |
| var trainers = Path.Combine(DEST_MASTER, "world/data/trainer/trdata/trdata_array.bin"); | |
| var table = FlatBufferConverter.DeserializeFrom<TrDataMainArray>(trainers).Table; | |
| foreach (var lang in LanguagesToIterate) | |
| { | |
| var dest = Path.Combine(DEST_MASTER, $"message/dat/{lang}/common/"); | |
| var lines = GetStringsCommon(lang, "trmsg"); | |
| var flags = GetStringFlagsCommon(lang, "trmsg"); | |
| var lva = GetLevelAbbreviation(lang); | |
| var sep = GetRangeSeparator(lang); | |
| for (int i = 0; i < lines.Length; i++) | |
| { | |
| var entry = entries[i]; | |
| if (!entry.Name.EndsWith("_POPUP") || lines[i].StartsWith("[~ ") || string.IsNullOrWhiteSpace(lines[i])) | |
| continue; | |
| bool clan = entry.Name.StartsWith("TRID_SP_TRAINER_0"); | |
| var check = entry.Name.Replace("TRID_", "TRNAME_").Replace("_TRMSG_FIGHT_POPUP", ""); | |
| // Kitakami trainers have two variants, for pre-postgame and postgame | |
| if (entry.Name.Contains("DLC1")) | |
| { | |
| var trWeak = table.FirstOrDefault(z => z.TrId == check.Replace("TRNAME_", "").ToLower()); | |
| var teamWeak = new[] { trWeak.Poke1, trWeak.Poke2, trWeak.Poke3, trWeak.Poke4, trWeak.Poke5, trWeak.Poke6 }; | |
| teamWeak = teamWeak.Where(z => z.DevId != DevID.DEV_NULL).ToArray(); | |
| var lvLo1 = teamWeak[0].Level; | |
| var lvHi1 = teamWeak[teamWeak.Length - 1].Level; | |
| var lvDisplay1 = lvLo1 == lvHi1 ? $"\\n({lva}{lvLo1})" : $"\\n({lva}{lvLo1}{sep}{lvHi1})"; | |
| var trStrong = table.FirstOrDefault(z => z.TrId == check.Replace("TRNAME_", "").ToLower() + "_strong"); | |
| var teamStrong = new[] { trStrong.Poke1, trStrong.Poke2, trStrong.Poke3, trStrong.Poke4, trStrong.Poke5, trStrong.Poke6 }; | |
| teamStrong = teamStrong.Where(z => z.DevId != DevID.DEV_NULL).ToArray(); | |
| var lvLo2 = teamStrong[0].Level; | |
| var lvHi2 = teamStrong[teamStrong.Length - 1].Level; | |
| var lvDisplay2 = lvLo2 == lvHi2 ? $"\\n({lva}{lvLo2})" : $"\\n({lva}{lvLo2}{sep}{lvHi2})"; | |
| lines[i] = lines[i] + lvDisplay1 + lvDisplay2; | |
| } | |
| else | |
| { | |
| var tr = clan ? table.FirstOrDefault(z => z.TrId.ToUpper() == check.Replace("TRNAME_", "")) : table.FirstOrDefault(z => z.TrNameLabel.ToUpper() == check); | |
| if (tr != null) | |
| { | |
| var team = new[] { tr.Poke1, tr.Poke2, tr.Poke3, tr.Poke4, tr.Poke5, tr.Poke6 }; | |
| team = team.Where(z => z.DevId != DevID.DEV_NULL).OrderBy(z => z.Level).ToArray(); | |
| var lvLo = team[0].Level; | |
| var lvHi = team[team.Length - 1].Level; | |
| var lvDisplay = lvLo == lvHi ? $"\\n({lva}{lvLo})" : $"\\n({lva}{lvLo}{sep}{lvHi})"; | |
| lines[i] = lines[i] + lvDisplay; | |
| } | |
| } | |
| } | |
| if (!Directory.Exists(dest)) | |
| Directory.CreateDirectory(dest); | |
| var write = TextFile.GetBytes(lines, flags, TextConfig); | |
| File.WriteAllBytes(Path.Combine(dest, "trmsg.dat"), write); | |
| } | |
| } | |
| private static void UpdateInGameText() | |
| { | |
| "Updating in-game text...".Dump(); | |
| var gifts = Path.Combine(DEST_MASTER, "world/data/event/event_add_pokemon/eventAddPokemon/eventAddPokemon_array.bin"); | |
| var statics = Path.Combine(DEST_MASTER, "world/data/battle/eventBattlePokemon/eventBattlePokemon_array.bin"); | |
| var trainers = Path.Combine(DEST_MASTER, "world/data/trainer/trdata/trdata_array.bin"); | |
| var trades = Path.Combine(DEST_MASTER, "world/data/event/eventTradeList/eventTradeList_array.bin"); | |
| var tableG = FlatBufferConverter.DeserializeFrom<EventAddPokemonArray>(gifts).Table; | |
| var tableS = FlatBufferConverter.DeserializeFrom<EventBattlePokemonArray>(statics).Table; | |
| var tableT = FlatBufferConverter.DeserializeFrom<TrDataMainArray>(trainers).Table; | |
| var tableI = FlatBufferConverter.DeserializeFrom<EventTradeListArray>(trades).Table; | |
| var entries = GetEntryListAHTBCommon(LANGUAGE, "zkn_type"); | |
| // gift | |
| var starterF = tableG[000].PokeData; | |
| var starterG = tableG[001].PokeData; | |
| var starterW = tableG[002].PokeData; | |
| var iguanaA = tableG[005].PokeData.DevId; | |
| var iguanaB = tableG[006].PokeData.DevId; | |
| var growlithe = tableG[008].PokeData.DevId; | |
| var meowth = tableG[012].PokeData.DevId; | |
| // static | |
| var lechonk = tableS[024].PokeData.DevId; | |
| var houndoom = tableS[025].PokeData.DevId; | |
| var sunflora = tableS[028].PokeData.DevId; | |
| var titanO = tableS[036].PokeData.DevId; | |
| var titanB = tableS[042].PokeData.DevId; | |
| var titanK = tableS[044].PokeData.DevId; | |
| var titanP = tableS[048].PokeData.DevId; | |
| var treasure1 = tableS[049].PokeData.DevId; | |
| var treasure2 = tableS[050].PokeData.DevId; | |
| var treasure3 = tableS[051].PokeData.DevId; | |
| var treasure4 = tableS[052].PokeData.DevId; | |
| var ogerpon = tableS[065].PokeData.DevId; | |
| var okidogi = tableS[069].PokeData.DevId; | |
| var fezandipiti = tableS[071].PokeData.DevId; | |
| var munkidori = tableS[073].PokeData.DevId; | |
| var ariados = tableS[074].PokeData.DevId; | |
| var ursaluna = tableS[075].PokeData.DevId; | |
| var glimmora = tableS[079].PokeData.DevId; | |
| var terapagos = tableS[088].PokeData.DevId; | |
| var meloetta = tableS[114].PokeData.DevId; | |
| // trainer | |
| var arven1 = tableT.FirstOrDefault(z => z.TrId == "pepper_nusi_01").Poke1.DevId; | |
| var arven2 = tableT.FirstOrDefault(z => z.TrId == "pepper_nusi_02").Poke1.DevId; | |
| var arven4 = tableT.FirstOrDefault(z => z.TrId == "pepper_nusi_04").Poke1.DevId; | |
| // trade | |
| var trade1 = tableI[000].SendPokeDevId; | |
| var trade2 = tableI[001].SendPokeDevId; | |
| var trade3 = tableI[002].SendPokeDevId; | |
| var typesG = GetPokemonTypes(starterG.DevId, starterG.FormId); | |
| var typesF = GetPokemonTypes(starterF.DevId, starterF.FormId); | |
| var typesW = GetPokemonTypes(starterW.DevId, starterW.FormId); | |
| // Starter introduction | |
| foreach (var lang in LanguagesToIterate) | |
| { | |
| var dest = Path.Combine(DEST_MASTER, $"message/dat/{lang}/script/"); | |
| var names = GetStringsCommon(lang, "monsname"); | |
| var categories = GetStringsCommon(lang, "zkn_type"); | |
| var lines = GetStringsScript(lang, "common_0060"); | |
| var flags = GetStringFlagsScript(lang, "common_0060"); | |
| var catOldG = GetCategoryString(entries, categories, 0906, 0); | |
| var catOldF = GetCategoryString(entries, categories, 0909, 0); | |
| var catOldW = GetCategoryString(entries, categories, 0912, 0); | |
| var catNewG = GetCategoryString(entries, categories, (ushort)starterG.DevId, starterG.FormId); | |
| var catNewF = GetCategoryString(entries, categories, (ushort)starterF.DevId, starterF.FormId); | |
| var catNewW = GetCategoryString(entries, categories, (ushort)starterW.DevId, starterW.FormId); | |
| lines[4] = lines[4].Replace(names[0906], names[(ushort)starterG.DevId]).Replace(catOldG, catNewG); // Sprigatito | |
| lines[6] = lines[6].Replace(names[0909], names[(ushort)starterF.DevId]).Replace(catOldF, catNewF); // Fuecoco | |
| lines[8] = lines[8].Replace(names[0912], names[(ushort)starterW.DevId]).Replace(catOldW, catNewW); // Quaxly | |
| // one half of the category string is after a line feed | |
| if (lang == "Spanish") | |
| { | |
| catOldF = catOldF.Replace(" ", "\\n"); | |
| catOldW = catOldW.Replace(" ", "\\n"); | |
| catNewF = catNewF.Replace(" ", "\\n"); | |
| catNewW = catNewW.Replace(" ", "\\n"); | |
| lines[6] = lines[6].Replace(catOldF, catNewF); | |
| lines[8] = lines[8].Replace(catOldW, catNewW); | |
| } | |
| if (!Directory.Exists(dest)) | |
| Directory.CreateDirectory(dest); | |
| var write = TextFile.GetBytes(lines, flags, TextConfig); | |
| File.WriteAllBytes(Path.Combine(dest, "common_0060.dat"), write); | |
| } | |
| // Starter selection | |
| foreach (var lang in LanguagesToIterate) | |
| { | |
| var dest = Path.Combine(DEST_MASTER, $"message/dat/{lang}/script/"); | |
| var names = GetStringsCommon(lang, "monsname"); | |
| var types = GetStringsCommon(lang, "typename"); | |
| var lines = GetStringsScript(lang, "common_0080"); | |
| var flags = GetStringFlagsScript(lang, "common_0080"); | |
| // Sprigatito | |
| lines[01] = lines[01].Replace(names[0906], names[(ushort)starterG.DevId]).Replace(types[11], types[typesG.Type1]); | |
| lines[02] = lines[02].Replace(names[0906], names[(ushort)starterG.DevId]).Replace(types[11], types[typesG.Type1]); | |
| // Fuecoco | |
| lines[05] = lines[05].Replace(names[0909], names[(ushort)starterF.DevId]).Replace(types[09], types[typesF.Type1]); | |
| lines[06] = lines[06].Replace(names[0909], names[(ushort)starterF.DevId]).Replace(types[09], types[typesF.Type1]); | |
| // Quaxly | |
| lines[09] = lines[09].Replace(names[0912], names[(ushort)starterW.DevId]).Replace(types[10], types[typesW.Type1]); | |
| lines[10] = lines[10].Replace(names[0912], names[(ushort)starterW.DevId]).Replace(types[10], types[typesW.Type1]); | |
| // grammar | |
| if (lang == "English" && GetStartsWithVowel(types[typesG.Type1])) lines[01] = lines[01].Replace($"a {types[typesG.Type1]}", $"an {types[typesG.Type1]}"); | |
| if (lang == "English" && GetStartsWithVowel(types[typesF.Type1])) lines[05] = lines[05].Replace($"a {types[typesF.Type1]}", $"an {types[typesF.Type1]}"); | |
| if (lang == "English" && GetStartsWithVowel(types[typesW.Type1])) lines[09] = lines[09].Replace($"a {types[typesW.Type1]}", $"an {types[typesW.Type1]}"); | |
| if (!Directory.Exists(dest)) | |
| Directory.CreateDirectory(dest); | |
| var write = TextFile.GetBytes(lines, flags, TextConfig); | |
| File.WriteAllBytes(Path.Combine(dest, "common_0080.dat"), write); | |
| } | |
| // Champion Assessment | |
| foreach (var lang in LanguagesToIterate) | |
| { | |
| var dest = Path.Combine(DEST_MASTER, $"message/dat/{lang}/script/"); | |
| var categories = GetStringsCommon(lang, "zkn_type"); | |
| var types = GetStringsCommon(lang, "typename"); | |
| var lines = GetStringsScript(lang, "champ_0180"); | |
| var flags = GetStringFlagsScript(lang, "champ_0180"); | |
| lines[063] = GetCategoryString(entries, categories, (ushort)starterG.DevId, starterG.FormId); | |
| lines[064] = GetCategoryString(entries, categories, (ushort)starterF.DevId, starterF.FormId); | |
| lines[065] = GetCategoryString(entries, categories, (ushort)starterW.DevId, starterW.FormId); | |
| // localization-specific quirks | |
| if (lang == "English") | |
| { | |
| lines[063] = "The " + lines[063]; | |
| lines[064] = "The " + lines[064]; | |
| lines[065] = "The " + lines[065]; | |
| } | |
| if (lang == "French") | |
| { | |
| lines[063] = lines[063] + "."; | |
| lines[064] = lines[064] + "."; | |
| lines[065] = lines[065] + "."; | |
| } | |
| if (lang == "German") | |
| { | |
| lines[063] = lines[063].Replace("-Pokémon", ""); | |
| lines[064] = lines[064].Replace("-Pokémon", ""); | |
| lines[065] = lines[065].Replace("-Pokémon", ""); | |
| } | |
| lines[102] = GetTypeDisplayString(types, lang, typesG); | |
| lines[103] = GetTypeDisplayString(types, lang, typesF); | |
| lines[104] = GetTypeDisplayString(types, lang, typesW); | |
| if (TYPE_THEMED_IMPORTANT_TRAINERS != TypeThemeRandomizer.NotTypeThemed) | |
| { | |
| lines[051] = types[ThemedTypes[3]]; // Kofu | |
| lines[052] = types[ThemedTypes[1]]; // Brassius | |
| lines[053] = types[ThemedTypes[2]]; // Iono | |
| lines[054] = types[ThemedTypes[7]]; // Grusha | |
| lines[055] = types[ThemedTypes[0]]; // Katy | |
| lines[056] = types[ThemedTypes[5]]; // Ryme | |
| lines[057] = types[ThemedTypes[6]]; // Tulip | |
| lines[058] = types[ThemedTypes[4]]; // Larry | |
| // only localization to not just list the type name | |
| if (lang == "French") | |
| { | |
| for (int i = 51; i <= 58; i++) | |
| lines[i] = $"Le type {lines[i]}."; | |
| } | |
| } | |
| if (!Directory.Exists(dest)) | |
| Directory.CreateDirectory(dest); | |
| var write = TextFile.GetBytes(lines, flags, TextConfig); | |
| File.WriteAllBytes(Path.Combine(dest, "champ_0180.dat"), write); | |
| } | |
| // Habitats -> Abilities | |
| if (DISPLAY_ABILITIES_IN_POKEDEX) | |
| { | |
| var abils = new[] { "Abilities", "Talent(s)", "Fähigkeiten", "Abilità", "とくせい", "とくせい", "특성", "特性", "Habilidades", "特性" }; | |
| foreach (var lang in LanguagesToIterate) | |
| { | |
| var dest = Path.Combine(DEST_MASTER, $"message/dat/{lang}/common/"); | |
| var lines = GetStringsCommon(lang, "pokedex"); | |
| var flags = GetStringFlagsCommon(lang, "pokedex"); | |
| var index = Languages.IndexOf(lang); | |
| var abil = abils[index]; | |
| lines[221] = lines[280] = abil; | |
| if (!Directory.Exists(dest)) | |
| Directory.CreateDirectory(dest); | |
| var write = TextFile.GetBytes(lines, flags, TextConfig); | |
| File.WriteAllBytes(Path.Combine(dest, "pokedex.dat"), write); | |
| } | |
| } | |
| // Badge type names on the map | |
| if (TYPE_THEMED_IMPORTANT_TRAINERS == TypeThemeRandomizer.RandomTypeThemes) | |
| { | |
| var badgeG = new[] { "Gym Badge ({0})", "Badge d’Arène {0}", "{0}-Arenaorden", "Medaglia Palestra {0}", "ジムバッジ:{0}", "ジムバッジ:{0}", "체육관 배지: {0}", "道馆徽章:{0}", "Medalla de Gimnasio {0}", "道館徽章:{0}" }; | |
| var badgeS = new[] { "Star Badge ({0})", "Badge Team {0}", "{0}-Teamorden", "Medaglia Team {0}", "ダンバッジ:{0}", "ダンバッジ:{0}", "단 배지: {0}", "团队徽章:{0}", "Medalla Star {0}", "團隊徽章:{0}" }; | |
| var badgeT = new[] { "Titan Badge ({0})", "Badge Dominant {0}", "{0}-Herrscherorden", "Medaglia Dominante {0}", "ヌシバッジ:{0}", "ヌシバッジ:{0}", "주인 배지: {0}", "宝主徽章:{0}", "Medalla Dominante {0}", "寶主徽章:{0}" }; | |
| foreach (var lang in LanguagesToIterate) | |
| { | |
| var dest = Path.Combine(DEST_MASTER, $"message/dat/{lang}/common/"); | |
| var names = GetStringsCommon(lang, "typename"); | |
| var lines = GetStringsCommon(lang, "ymap_mission_reward"); | |
| var flags = GetStringFlagsCommon(lang, "ymap_mission_reward"); | |
| var index = Languages.IndexOf(lang); | |
| lines[09] = string.Format(badgeG[index], names[ThemedTypes[02]]); // Iono | |
| lines[10] = string.Format(badgeG[index], names[ThemedTypes[06]]); // Tulip | |
| lines[11] = string.Format(badgeG[index], names[ThemedTypes[05]]); // Ryme | |
| lines[12] = string.Format(badgeG[index], names[ThemedTypes[07]]); // Grusha | |
| lines[13] = string.Format(badgeG[index], names[ThemedTypes[01]]); // Brassius | |
| lines[14] = string.Format(badgeG[index], names[ThemedTypes[03]]); // Kofu | |
| lines[15] = string.Format(badgeG[index], names[ThemedTypes[00]]); // Katy | |
| lines[16] = string.Format(badgeG[index], names[ThemedTypes[04]]); // Larry | |
| lines[17] = string.Format(badgeS[index], names[ThemedTypes[11]]); // Ortega | |
| lines[18] = string.Format(badgeS[index], names[ThemedTypes[10]]); // Atticus | |
| lines[19] = string.Format(badgeS[index], names[ThemedTypes[12]]); // Eri | |
| lines[20] = string.Format(badgeS[index], names[ThemedTypes[08]]); // Giacomo | |
| lines[21] = string.Format(badgeS[index], names[ThemedTypes[09]]); // Mela | |
| lines[22] = string.Format(badgeT[index], names[ThemedTypes[17]]); // Tatsugiri / Dondozo | |
| lines[23] = string.Format(badgeT[index], names[ThemedTypes[15]]); // Orthworm | |
| lines[24] = string.Format(badgeT[index], names[ThemedTypes[14]]); // Bombirdier | |
| lines[25] = string.Format(badgeT[index], names[ThemedTypes[13]]); // Klawf | |
| lines[26] = string.Format(badgeT[index], names[ThemedTypes[16]]); // Great Tusk / Iron Treads | |
| if (!Directory.Exists(dest)) | |
| Directory.CreateDirectory(dest); | |
| var write = TextFile.GetBytes(lines, flags, TextConfig); | |
| File.WriteAllBytes(Path.Combine(dest, "ymap_mission_reward.dat"), write); | |
| } | |
| } | |
| // Common | |
| UpdateTextSimpleSpecies("common", "btl_std", [1021], [terapagos], [607,608]); | |
| UpdateTextSimpleSpecies("common", "gymtest", [0192], [sunflora], [7]); | |
| UpdateTextSimpleSpecies("common", "hud_notice", [0192], [sunflora], [48]); | |
| UpdateTextSimpleSpecies("common", "original_book", [1021], [terapagos], [62]); | |
| UpdateTextSimpleSpecies("common", "ymap_mission_add", [1014, 1015, 1016], [okidogi, munkidori, fezandipiti], [3,3,3]); | |
| UpdateTextSimpleSpecies("common", "ymap_mission_character", [1014, 1015, 1016], [okidogi, munkidori, fezandipiti], [18,19,20]); | |
| UpdateTextSimpleSpecies("common", "ymap_mission_character_title", [1014, 1015, 1016], [okidogi, munkidori, fezandipiti], [18,19,20]); | |
| UpdateTextSimpleSpecies("common", "ymap_mission_guide", [0192, 1011, 1011, 1011, 1011, 1011, 1014, 1015, 1016], [sunflora, ogerpon, ogerpon, ogerpon, ogerpon, ogerpon, okidogi, munkidori, fezandipiti], [38,120,128,129,130,131,137,138,139]); | |
| UpdateTextSimpleSpecies("common", "ymap_mission_title", [0192, 1011, 1014, 1015, 1016], [sunflora, ogerpon, okidogi, munkidori, fezandipiti], [45,128, 156,157,158]); | |
| UpdateTextSimpleSpecies("common", "ymap_title", [0192, 1011, 1011, 1014, 1015, 1016], [sunflora, ogerpon, ogerpon, okidogi, munkidori, fezandipiti], [16,156,158,172,173,174]); | |
| // Script | |
| UpdateTextSimpleSpecies("script", "bg_event", [0998, 0999, 0998, 0999, 1021, 1021, 1021, 1021], [iguanaA, iguanaB, iguanaA, iguanaB, terapagos, terapagos, terapagos, terapagos], [235,236,241,242,449,450,451,452]); | |
| UpdateTextSimpleSpecies("script", "btl_talk", [0090, 0978, 0986, 0939, 1006, 0998, 0999, 0998, 0999, 1011, 1011, 1011, 1011, 1011, 1011, 1011, 1011, 1011, 1011, 1011, 1021, 1021, 1021, 1021], [arven1, titanP, titanP, arven4, arven2, iguanaA, iguanaB, iguanaA, iguanaB, ogerpon, ogerpon, ogerpon, ogerpon, ogerpon, ogerpon, ogerpon, ogerpon, ogerpon, ogerpon, ogerpon, terapagos, terapagos, terapagos, terapagos], [24,30,31,34,36,317,318,323,324,1055,1057,1064,1066,1067,1068,1072,1088,1089,1090,1101,1130,1135,1139,1140]); | |
| UpdateTextSimpleSpecies("script", "common_0100", [0915], [lechonk], [8]); | |
| UpdateTextSimpleSpecies("script", "common_0170", [0229], [houndoom], [12]); | |
| UpdateTextSimpleSpecies("script", "common_1200", [0978, 0986], [titanP], [49,50]); | |
| UpdateTextSimpleSpecies("script", "kitakami_center", [1016, 1015, 1015, 1014], [fezandipiti, munkidori, munkidori, okidogi], [13,14,16,19]); | |
| UpdateTextSimpleSpecies("script", "kizuna_language_d", [0052], [meowth], [22]); | |
| UpdateTextSimpleSpecies("script", "nushi_hagane_020", [0944], [titanO], [0]); | |
| UpdateTextSimpleSpecies("script", "nushi_hikou_020", [0959], [titanB], [0]); | |
| UpdateTextSimpleSpecies("script", "nushi_iwa_020", [0962], [titanK], [0,2]); | |
| UpdateTextSimpleSpecies("script", "nushi_jimen_020", [0978, 0986], [titanP], [0,1]); | |
| UpdateTextSimpleSpecies("script", "popup", [0192, 0669, 0194, 0871, 0192, 1011, 1011, 1011, 1014, 1014, 1011, 1015, 1015, 1014, 1016, 1014, 1015], [sunflora, trade3, trade1, trade2, sunflora, ogerpon, ogerpon, ogerpon, okidogi, okidogi, ogerpon, munkidori, munkidori, okidogi, fezandipiti, okidogi, munkidori], [1491,1509,1510,1511,1526,1550,1559,1563,1610,1611,1612,1669,1671,1674,1788,1789,1790]); | |
| UpdateTextSimpleSpecies("script", "s2_sub_042", [0998, 0999, 0998, 0999, 0998, 0999, 0998, 0999, 1021, 1021], [iguanaA, iguanaB, iguanaA, iguanaB, iguanaA, iguanaB, iguanaA, iguanaB, terapagos, terapagos], [22,23,24,25,26,27,87,88,103,104]); | |
| UpdateTextSimpleSpecies("script", "sdc01_0380", [1016, 1014, 1015], [fezandipiti, okidogi, munkidori], [0,1,2]); | |
| UpdateTextSimpleSpecies("script", "sdc01_side01_010", [1014, 1015, 1016, 0998, 0999, 0998, 0999], [okidogi, munkidori, fezandipiti, iguanaA, iguanaB, iguanaA, iguanaB], [8,8,8,33,34,36,37]); | |
| UpdateTextSimpleSpecies("script", "sdc02_0262", [0967, 1021], [glimmora, terapagos], [27,27]); | |
| UpdateTextSimpleSpecies("script", "t10", [1011, 1014, 1014, 1011], [ogerpon, okidogi, okidogi, ogerpon], [15,16,17,18]); | |
| // Artazon Gym Challenge | |
| UpdateTextSimpleSpecies("script", "gym_kusa_010", [0192], [sunflora], [3,4,5,6,7]); | |
| UpdateTextSimpleSpecies("script", "gym_kusa_020", [0192], [sunflora], [1,2,5,7,8,12,13,18,19,21,22,24,26,28,29,31,32,33,34]); | |
| UpdateTextSimpleSpecies("script", "gym_kusa_030", [0192], [sunflora], [0]); | |
| UpdateTextSimpleSpecies("script", "gym_kusa_040", [0192], [sunflora], [9]); | |
| UpdateTextSimpleSpecies("script", "sub_001", [0192], [sunflora], [42,43,44,48,49,50,51,54,55,56,57,60,61,62,64,66,67,68,69,70,71,72,73,75,77,78,81,82,83,84,85,86]); | |
| // Perrin's Growlithe | |
| UpdateTextSimpleSpecies("script", "s1_side02_0000", [0058], [growlithe], [8]); | |
| UpdateTextSimpleSpecies("script", "s1_side02_0020", [0058], [growlithe], [23]); | |
| UpdateTextSimpleSpecies("script", "s1_side02_0030", [0058, 0168], [growlithe, ariados], [24,30]); | |
| UpdateTextSimpleSpecies("script", "s1_side02_0050", [0058], [growlithe], [7,28,40]); | |
| UpdateTextSimpleSpecies("script", "s1_side02_0060", [0058], [growlithe], [22,24,28,31]); | |
| UpdateTextSimpleSpecies("script", "s1_side02_fieldwork", [0058], [growlithe], [61]); | |
| UpdateTextSimpleSpecies("script", "s2_side02_0000", [0058], [growlithe], [10,12,14,15,17]); | |
| UpdateTextSimpleSpecies("script", "s2_side02_0030", [0058], [growlithe], [25]); | |
| // Bloodmoon Ursaluna | |
| UpdateTextSimpleSpecies("script", "s1_side02_0020", [0901], [ursaluna], [8,9,10,12]); | |
| // Meloetta | |
| UpdateTextSimpleSpecies("script", "s2_sub_003", [0648], [meloetta], [1]); | |
| // Treasures of Ruin | |
| UpdateTextSimpleSpecies("script", "kizuna_history_d", [0994, 0995, 0996, 0997], [treasure1, treasure2, treasure3, treasure4], [3,4,5,6]); | |
| UpdateTextSimpleSpecies("script", "sub_014", [0994], [treasure1], [10]); // Ting-Lu | |
| UpdateTextSimpleSpecies("script", "sub_015", [0995], [treasure2], [10]); // Chien-Pao | |
| UpdateTextSimpleSpecies("script", "sub_016", [0996], [treasure3], [10]); // Wo-Chien | |
| UpdateTextSimpleSpecies("script", "sub_017", [0997], [treasure4], [10]); // Chi-Yu | |
| // Snacksworth Legendary Pokémon | |
| for (int i = 13, k = 0; i <= 37; i++, k++) | |
| { | |
| var snack = tableS[SnacksworthEncounterIndices[k]].PokeData.DevId; | |
| var original = SnacksworthOriginalSpecies[k]; | |
| UpdateTextSimpleSpecies("script", $"s2_sub_{i:000}", [original], [snack], [1]); | |
| } | |
| var snackSpeciesOrig = SnacksworthOriginalSpecies.ToArray(); | |
| var snackSpeciesRand = SnacksworthEncounterIndices.Select(z => tableS[z].PokeData.DevId).ToArray(); | |
| var snackSpeciesRange = Enumerable.Range(2522, 25).ToArray(); | |
| UpdateTextSimpleSpecies("common", "iteminfo", snackSpeciesOrig, snackSpeciesRand, snackSpeciesRange); | |
| UpdateTextSimpleSpecies("common", "itemname", snackSpeciesOrig, snackSpeciesRand, snackSpeciesRange); | |
| UpdateTextSimpleSpecies("common", "itemname_acc", snackSpeciesOrig, snackSpeciesRand, snackSpeciesRange); | |
| UpdateTextSimpleSpecies("common", "itemname_acc_classified", snackSpeciesOrig, snackSpeciesRand, snackSpeciesRange); | |
| UpdateTextSimpleSpecies("common", "itemname_classified", snackSpeciesOrig, snackSpeciesRand, snackSpeciesRange); | |
| UpdateTextSimpleSpecies("common", "itemname_plural", snackSpeciesOrig, snackSpeciesRand, snackSpeciesRange); | |
| UpdateTextSimpleSpecies("common", "itemname_plural_classified", snackSpeciesOrig, snackSpeciesRand, snackSpeciesRange); | |
| var snackDialogueOrig = new int[75]; | |
| var snackDialogueRand = new DevID[75]; | |
| var snackDialogueRange = Enumerable.Range(26, 75).ToArray(); | |
| var ctrOrig = 0; | |
| var ctrRand = 0; | |
| for (int i = 0; i < snackSpeciesOrig.Length; i++) { for (int l = 0; l < 3; l++) { snackDialogueOrig[ctrOrig] = snackSpeciesOrig[i]; ctrOrig++; } } | |
| for (int i = 0; i < snackSpeciesRand.Length; i++) { for (int l = 0; l < 3; l++) { snackDialogueRand[ctrRand] = snackSpeciesRand[i]; ctrRand++; } } | |
| UpdateTextSimpleSpecies("script", "s2_sub_012", snackDialogueOrig, snackDialogueRand, snackDialogueRange); | |
| // Okidogi | |
| UpdateTextSimpleSpecies("script", "s1_sub_011", [1014], [okidogi], [1]); | |
| UpdateTextSimpleSpecies("script", "sdc01_3gods_a", [1014], [okidogi], [0]); | |
| // Munkidori | |
| UpdateTextSimpleSpecies("script", "s1_sub_012", [1015], [munkidori], [1]); | |
| UpdateTextSimpleSpecies("script", "sdc01_0340", [1015], [munkidori], [22]); | |
| UpdateTextSimpleSpecies("script", "sdc01_3gods_b", [1015], [munkidori], [0]); | |
| // Fezandipiti | |
| UpdateTextSimpleSpecies("script", "s1_sub_016", [1016], [fezandipiti], [1]); | |
| UpdateTextSimpleSpecies("script", "sdc01_3gods_c", [1016], [fezandipiti], [0]); | |
| // Ogerpon | |
| UpdateTextSimpleSpecies("script", "a_t10_i01", [1011], [ogerpon], [3,7]); | |
| UpdateTextSimpleSpecies("script", "s1_sub_015", [1011], [ogerpon], [2,3]); | |
| UpdateTextSimpleSpecies("script", "s2_side01_0075", [1011], [ogerpon], [19]); | |
| UpdateTextSimpleSpecies("script", "sdc01_0220", [1011], [ogerpon], [26,28]); | |
| UpdateTextSimpleSpecies("script", "sdc01_0260", [1011], [ogerpon], [1,18,27,30]); | |
| UpdateTextSimpleSpecies("script", "sdc01_0290", [1011], [ogerpon], [2,7,10,11]); | |
| UpdateTextSimpleSpecies("script", "sdc01_0350", [1011], [ogerpon], [12,18,20,24,28]); | |
| UpdateTextSimpleSpecies("script", "sdc01_0360", [1011], [ogerpon], [28,34,39,41,42,52]); | |
| UpdateTextSimpleSpecies("script", "sdc01_0370", [1011], [ogerpon], [1,5,8,9]); | |
| UpdateTextSimpleSpecies("script", "sdc01_0390", [1011], [ogerpon], [1,19,25]); | |
| UpdateTextSimpleSpecies("script", "sdc01_0410", [1011], [ogerpon], [5]); | |
| UpdateTextSimpleSpecies("script", "sdc01_0420", [1011], [ogerpon], [3,5,14]); | |
| UpdateTextSimpleSpecies("script", "sdc01_0430", [1011], [ogerpon], [4,6,12,20,28,30,31]); | |
| UpdateTextSimpleSpecies("script", "sdc01_0440", [1011], [ogerpon], [2,3,4,8,12,16,18,19]); | |
| UpdateTextSimpleSpecies("script", "sdc01_0450", [1011], [ogerpon], [3,10]); | |
| UpdateTextSimpleSpecies("script", "sdc01_0460", [1011], [ogerpon], [0,1,3,14]); | |
| UpdateTextSimpleSpecies("script", "sdc01_0480", [1011], [ogerpon], [9,10,11,14,15,16,20,21]); | |
| UpdateTextSimpleSpecies("script", "sdc02_0280", [1011], [ogerpon], [12]); | |
| // Terapagos | |
| UpdateTextSimpleSpecies("script", "sdc01_0020", [1021], [terapagos], [13,15]); | |
| UpdateTextSimpleSpecies("script", "sdc01_0300", [1021], [terapagos], [31]); | |
| UpdateTextSimpleSpecies("script", "sdc02_0230", [1021], [terapagos], [21,22]); | |
| UpdateTextSimpleSpecies("script", "sdc02_0260", [1021], [terapagos], [13,14,15,16,17,21]); | |
| UpdateTextSimpleSpecies("script", "sdc02_0290", [1021], [terapagos], [1]); | |
| UpdateTextSimpleSpecies("script", "sdc02_0300", [1021], [terapagos], [0,4,5,7,10]); | |
| UpdateTextSimpleSpecies("script", "sdc02_0320", [1021], [terapagos], [1,9,11,12,13]); | |
| UpdateTextSimpleSpecies("script", "sdc02_0330", [1021], [terapagos], [2,5,7]); | |
| UpdateTextSimpleSpecies("script", "sdc02_0340", [1021], [terapagos], [26]); | |
| UpdateTextSimpleSpecies("script", "sdc02_0370", [1021], [terapagos], [11,22]); | |
| if (RANDOMIZE_KORAIDON_MIRAIDON) | |
| { | |
| UpdateTextSimpleSpecies("common", "iteminfo", [0998, 0999], [iguanaA], [1831,1832]); | |
| UpdateTextSimpleSpecies("common", "itemname", [0998, 0999], [iguanaA], [1831,1832]); | |
| UpdateTextSimpleSpecies("common", "itemname_acc", [0998, 0999], [iguanaA], [1831,1832]); | |
| UpdateTextSimpleSpecies("common", "itemname_acc_classified", [0998, 0999], [iguanaA], [1831,1832]); | |
| UpdateTextSimpleSpecies("common", "itemname_classified", [0998, 0999], [iguanaA], [1831,1832]); | |
| UpdateTextSimpleSpecies("common", "itemname_plural", [0998, 0999], [iguanaA], [1831,1832]); | |
| UpdateTextSimpleSpecies("common", "itemname_plural_classified", [0998, 0999], [iguanaA], [1831,1832]); | |
| UpdateTextSimpleSpecies("script", "atlantis_talk_05", [0998, 0999, 0998, 0999], [iguanaA], [0,1,18,19]); | |
| UpdateTextSimpleSpecies("script", "common_0270", [0998, 0999, 0998, 0999, 0998, 0999, 0998, 0999], [iguanaA], [29,30,35,36,39,40,41,42]); | |
| UpdateTextSimpleSpecies("script", "common_1100", [0998, 0999, 0998, 0999], [iguanaA], [6,7,54,55]); | |
| UpdateTextSimpleSpecies("script", "common_1190", [0998, 0999, 0998, 0999], [iguanaA], [12,13,29,30]); | |
| UpdateTextSimpleSpecies("script", "common_1290", [0998, 0999], [iguanaA], [62,63]); | |
| UpdateTextSimpleSpecies("script", "kizuna_botan_d", [0998, 0999], [iguanaA], [21,22]); | |
| UpdateTextSimpleSpecies("script", "pair_talk", [0998, 0999], [iguanaA], [620,621]); | |
| UpdateTextSimpleSpecies("script", "sdc02_0264", [0998, 0999], [iguanaA], [0,1]); | |
| UpdateTextSimpleSpecies("script", "sdc02_4kings_b_02", [0998, 0999], [iguanaA], [20,21]); | |
| UpdateTextSimpleSpecies("script", "sub_018", [0998, 0999], [iguanaA], [11,12]); | |
| UpdateTextVariableSpecies("script", "atlantis_talk_01", iguanaA, [3]); | |
| UpdateTextVariableSpecies("script", "atlantis_talk_06", iguanaA, [0,11,20]); | |
| UpdateTextVariableSpecies("script", "common_0185", iguanaA, [15,16,20,22,30]); | |
| UpdateTextVariableSpecies("script", "common_0310", iguanaA, [45]); | |
| UpdateTextVariableSpecies("script", "common_1000", iguanaA, [4]); | |
| UpdateTextVariableSpecies("script", "common_1020", iguanaA, [8,9,14,15,16]); | |
| UpdateTextVariableSpecies("script", "common_1150", iguanaA, [5,6,12,13,15]); | |
| UpdateTextVariableSpecies("script", "common_1170", iguanaA, [8,14]); | |
| UpdateTextVariableSpecies("script", "common_1180", iguanaA, [42]); | |
| UpdateTextVariableSpecies("script", "common_1305", iguanaA, [18,21]); | |
| UpdateTextVariableSpecies("script", "legend_0010", iguanaA, [5]); | |
| UpdateTextVariableSpecies("script", "legend_0020", iguanaA, [21,25,37,38,39,40,41]); | |
| UpdateTextVariableSpecies("script", "legend_0025", iguanaA, [3,4,15,16,17,18]); | |
| UpdateTextVariableSpecies("script", "legend_0030", iguanaA, [19,23,28,31,32,33,34,35]); | |
| UpdateTextVariableSpecies("script", "legend_0035", iguanaA, [3,4,15,16]); | |
| UpdateTextVariableSpecies("script", "legend_0040", iguanaA, [19,23,30,31,32,33,34]); | |
| UpdateTextVariableSpecies("script", "legend_0045", iguanaA, [3,4,15,16]); | |
| UpdateTextVariableSpecies("script", "legend_0050", iguanaA, [15,19,26,27,28,29,30]); | |
| UpdateTextVariableSpecies("script", "legend_0055", iguanaA, [3,4,15,16]); | |
| UpdateTextVariableSpecies("script", "legend_0060", iguanaA, [14,18,20,21,22,23,24]); | |
| UpdateTextVariableSpecies("script", "legend_0080", iguanaA, [5,6,41]); | |
| UpdateTextVariableSpecies("script", "s2_sub_039", iguanaA, [11]); | |
| } | |
| } | |
| private static void UpdateInBattleDialogue() | |
| { | |
| if (!RANDOMIZE_KORAIDON_MIRAIDON && !REMOVE_IN_BATTLE_TRAINER_DIALOGUE && !REMOVE_FORCED_BONUS_STAT_CHANGES) | |
| return; | |
| "Updating in-battle dialogue...".Dump(); | |
| const string file = "trenv_array.bin"; | |
| var dest = Path.Combine(DEST_MASTER, "world/data/trainer/trenv/"); | |
| var path = Path.Combine(PATH_MASTER, "world/data/trainer/trenv/"); | |
| var obj = FlatBufferConverter.DeserializeFrom<TrainerEnvArray>(Path.Combine(path, file)); | |
| bool IsParadise(string id) => id is "professor_A_02" or "professor_B_02"; | |
| if (RANDOMIZE_KORAIDON_MIRAIDON) | |
| { | |
| var gifts = Path.Combine(DEST_MASTER, "world/data/event/event_add_pokemon/eventAddPokemon/eventAddPokemon_array.bin"); | |
| var table = FlatBufferConverter.DeserializeFrom<EventAddPokemonArray>(gifts).Table; | |
| var pk = table.FirstOrDefault(z => z.Label == "gameclear_BIGUANA").PokeData; | |
| var baseStats = GetBaseStats(pk.DevId, pk.FormId); | |
| var iv = pk.TalentValue; | |
| var ev = pk.EffortValue; | |
| var level = pk.Level; | |
| var nature = pk.Seikaku; | |
| var ability = GetCurrentAbility(pk.DevId, pk.FormId, pk.Tokusei); | |
| // calculate stats | |
| var statATK = CalculateStatOther(baseStats[1], StatIndex.ATK, iv.ATK, ev.ATK, level, nature, ability, true); | |
| var statSPA = CalculateStatOther(baseStats[3], StatIndex.SPA, iv.SPA, ev.SPA, level, nature, ability, true); | |
| // +2 Atk if Atk is higher, +2 SpA if SpA is higher, +1 All Stats if Atk and SpA are tied | |
| var increase = statATK > statSPA ? TalkRankEffect.Attack2 : statATK == statSPA ? TalkRankEffect.Multi5 : TalkRankEffect.SpAttack2; | |
| foreach (var t in obj.Table.Where(z => IsParadise(z.TrEnvId))) | |
| { | |
| var talkData = new[] { t.TalkData1, t.TalkData2, t.TalkData3, t.TalkData4, t.TalkData5, t.TalkData6, t.TalkData7, t.TalkData8, t.TalkData9, t.TalkData10, t.TalkData11, t.TalkData12, t.TalkData13, t.TalkData14, t.TalkData15 }; | |
| var ctr = 8; | |
| foreach (var td in talkData) | |
| { | |
| // add in more stat boosts | |
| if (string.IsNullOrWhiteSpace(td.EventName)) | |
| { | |
| td.EventName = "ee_bk_professor_02_12"; | |
| td.Category = CheckCategory.turnEndAndTurnCount; | |
| td.ValueB = ctr; | |
| td.RankEffect = increase; | |
| ctr++; | |
| } | |
| // change existing stat boosts | |
| if (td.RankEffect != TalkRankEffect.None) | |
| td.RankEffect = increase; | |
| } | |
| } | |
| } | |
| // dialogue | |
| if (REMOVE_IN_BATTLE_TRAINER_DIALOGUE) | |
| { | |
| foreach (var t in obj.Table.Where(z => !IsParadise(z.TrEnvId))) | |
| { | |
| var talkData = new[] { t.TalkData1, t.TalkData2, t.TalkData3, t.TalkData4, t.TalkData5, t.TalkData6, t.TalkData7, t.TalkData8, t.TalkData9, t.TalkData10, t.TalkData11, t.TalkData12, t.TalkData13, t.TalkData14, t.TalkData15 }; | |
| foreach (var td in talkData.Where(z => z != null && z.RankEffect == TalkRankEffect.None)) | |
| { | |
| td.EventName = string.Empty; | |
| td.Category = CheckCategory.TurnCheck; | |
| td.ValueA = 0; | |
| td.ValueB = 0; | |
| td.ValueC = 0; | |
| td.RankEffect = TalkRankEffect.None; | |
| } | |
| } | |
| } | |
| // stat changes | |
| if (REMOVE_FORCED_BONUS_STAT_CHANGES) | |
| { | |
| foreach (var t in obj.Table.Where(z => !IsParadise(z.TrEnvId))) | |
| { | |
| var talkData = new[] { t.TalkData1, t.TalkData2, t.TalkData3, t.TalkData4, t.TalkData5, t.TalkData6, t.TalkData7, t.TalkData8, t.TalkData9, t.TalkData10, t.TalkData11, t.TalkData12, t.TalkData13, t.TalkData14, t.TalkData15 }; | |
| foreach (var td in talkData.Where(z => z != null && z.RankEffect != TalkRankEffect.None)) | |
| { | |
| td.EventName = string.Empty; | |
| td.Category = CheckCategory.TurnCheck; | |
| td.ValueA = 0; | |
| td.ValueB = 0; | |
| td.ValueC = 0; | |
| td.RankEffect = TalkRankEffect.None; | |
| } | |
| } | |
| } | |
| if (!Directory.Exists(dest)) | |
| Directory.CreateDirectory(dest); | |
| var data = FlatBufferConverter.SerializeFrom(obj); | |
| File.WriteAllBytes(Path.Combine(dest, file), data); | |
| } | |
| private static void UpdateLuaLogic() | |
| { | |
| "Updating Lua logic...".Dump(); | |
| const string file = "main.blua"; | |
| var dest = Path.Combine(DEST_MASTER, "script/lua/bin/release/main/"); | |
| var source = File.Exists(Path.Combine(dest, file)) ? Path.Combine(dest, file) : Path.Combine(PATH_MASTER, "script/lua/bin/release/main/", file); | |
| var data = File.ReadAllBytes(source); | |
| var replace = 1501; | |
| // False Dragon Titan: logic is hardcoded to check for Dondozo DevID, overwrite it with our randomized species | |
| var statics = Path.Combine(DEST_MASTER, "world/data/battle/eventBattlePokemon/eventBattlePokemon_array.bin"); | |
| var obj = FlatBufferConverter.DeserializeFrom<EventBattlePokemonArray>(statics); | |
| var dev = obj.Table.FirstOrDefault(z => z.Label == "nusi_931_02").PokeData.DevId; | |
| var arrDondozo = new byte[] { 0x66, 0x75, 0x74, 0x61, 0x74, 0x73, 0x75, 0x6E, 0x61, 0x5F, 0x6A, 0x69, 0x6D, 0x65, 0x6E, 0x5F, 0x62, 0x5F, 0x6E, 0x61, 0x6D, 0x65, 0x03 }; | |
| var ofsDondozo = GetOffsetFromByteArray(data, arrDondozo); | |
| WriteUInt16LittleEndian(data.AsSpan(ofsDondozo), (ushort)dev); | |
| // Scatterbug, Spewpa, Vivillon: as of the 1.2.0 update, the game normally forces form 18 (Fancy) for all wild spawns | |
| // The raw byte replacements below replace the 664,665,666 DevID checks with out of bounds IDs, allowing all forms to spawn | |
| // Thank you to Aria Seri and Kas Seri for providing the offsets and an explanation for how this works! | |
| byte[][] arrVivillon = | |
| [ | |
| new byte[] { 0x04, 0x8A, 0x66, 0x35, 0x33, 0x41, 0x43, 0x34, 0x32, 0x30, 0x35, 0x04, 0x8A, 0x66, 0x32, 0x42, 0x46, 0x33, 0x36, 0x36, 0x41, 0x32, 0x03 }, | |
| new byte[] { 0x00, 0xC8, 0x00, 0x02, 0x00, 0xB8, 0x00, 0x00, 0x80, 0x87, 0x00, 0x00, 0x00, 0xC8, 0x00, 0x02, 0x00, 0xC7, 0x00, 0x01, 0x00, 0x83, 0x03 }, | |
| new byte[] { 0x38, 0x39, 0x37, 0x04, 0x87, 0x5F, 0x5F, 0x63, 0x61, 0x73, 0x74, 0x04, 0x8A, 0x66, 0x42, 0x33, 0x31, 0x44, 0x46, 0x35, 0x38, 0x39, 0x03 }, | |
| ]; | |
| for (int i = 0; i < arrVivillon.Length; i++) | |
| { | |
| var pattern = arrVivillon[i]; | |
| for (int v = 0; v <= 2; v++) | |
| { | |
| var add = GetExtraOffsetAdd(v); | |
| var ofsVivillon = GetOffsetFromByteArray(data, pattern, add); | |
| WriteUInt16LittleEndian(data.AsSpan(ofsVivillon), (ushort)replace); | |
| replace++; | |
| } | |
| } | |
| if (REMOVE_DITTO_ZORUA_ZOROARK_WILD_DISGUISES) | |
| { | |
| byte[][] arrDisguise = | |
| [ | |
| new byte[] { 0x6E, 0x65, 0x77, 0x04, 0x8A, 0x63, 0x31, 0x31, 0x33, 0x33, 0x33, 0x35, 0x41, 0x38, 0x04, 0x8A, 0x66, 0x42, 0x39, 0x31, 0x41, 0x39, 0x30, 0x39, 0x43, 0x04, 0x8A, 0x66, 0x30, 0x42, 0x44, 0x35, 0x31, 0x33, 0x34, 0x46, 0x03 }, | |
| new byte[] { 0x41, 0x38, 0x30, 0x04, 0x8A, 0x66, 0x32, 0x42, 0x46, 0x33, 0x36, 0x36, 0x41, 0x32, 0x04, 0x8A, 0x66, 0x45, 0x31, 0x35, 0x45, 0x39, 0x44, 0x30, 0x41, 0x04, 0x8A, 0x66, 0x35, 0x42, 0x38, 0x41, 0x33, 0x46, 0x45, 0x32, 0x03 }, | |
| new byte[] { 0x32, 0x44, 0x35, 0x33, 0x32, 0x33, 0x04, 0x92, 0x53, 0x38, 0x43, 0x45, 0x44, 0x34, 0x33, 0x35, 0x39, 0x32, 0x30, 0x34, 0x35, 0x46, 0x31, 0x46, 0x31, 0x04, 0x8A, 0x66, 0x32, 0x42, 0x46, 0x33, 0x36, 0x36, 0x41, 0x32, 0x03 }, | |
| ]; | |
| for (int i = 0; i < arrDisguise.Length; i++) | |
| { | |
| var pattern = arrDisguise[i]; | |
| for (int v = 0; v <= 2; v++) | |
| { | |
| var add = GetExtraOffsetAdd(v); | |
| var ofsDisguise = GetOffsetFromByteArray(data, pattern, add); | |
| WriteUInt16LittleEndian(data.AsSpan(ofsDisguise), (ushort)replace); | |
| replace++; | |
| } | |
| } | |
| } | |
| if (!Directory.Exists(dest)) | |
| Directory.CreateDirectory(dest); | |
| File.WriteAllBytes(Path.Combine(dest, file), data); | |
| } | |
| private static int GetExtraOffsetAdd(int index) => index switch | |
| { | |
| 1 => 0x09, | |
| 2 => 0x12, | |
| _ => 0x00, | |
| }; | |
| private static int GetOffsetFromByteArray(byte[] arr, byte[] pattern, int forceAdd = 0) | |
| { | |
| for (int i = 0; i < arr.Length; i++) | |
| { | |
| if (arr.Skip(i).Take(pattern.Length).SequenceEqual(pattern)) | |
| return (i + pattern.Length + forceAdd); | |
| } | |
| return -1; | |
| } | |
| private static void AddPokemonBehaviorData() | |
| { | |
| AddNewUniquePathData("single"); | |
| AddNewUniquePathData("group"); | |
| AddNewUniquePathData("special"); | |
| AddNewBehaviorDataSingle(); | |
| AddNewBehaviorDataGroup(); | |
| AddNewBehaviorDataSpecial(); | |
| } | |
| private static void AddNewUniquePathData(string type) | |
| { | |
| var file = $"wild_pokemon_{type}_data.bin"; | |
| var dest = Path.Combine(DEST_MASTER, "world/data/pokemon/pokemon_unique_path_data/"); | |
| var path = Path.Combine(PATH_MASTER, "world/data/pokemon/pokemon_unique_path_data/"); | |
| var obj = FlatBufferConverter.DeserializeFrom<PokemonUniquePathData>(Path.Combine(path, file)); | |
| var idx = 0; | |
| var presentPaths = new List<string>(); | |
| var missingPaths = type switch | |
| { | |
| "single" => MissingBehaviorDataSingle, | |
| "group" => MissingBehaviorDataGroup, | |
| "special" => MissingBehaviorDataSpecial, | |
| _ => throw new ArgumentException(nameof(type), type), | |
| }; | |
| // gather paths that already exist, stop iterating when we've reached the last one | |
| for (int i = 0; i <= 1099; i++) | |
| { | |
| var field = obj.GetType().GetProperty($"Path{i:000}"); | |
| var value = field.GetValue(obj).ToString(); | |
| if (string.IsNullOrWhiteSpace(value)) | |
| { | |
| idx = i; | |
| break; | |
| } | |
| else | |
| { | |
| presentPaths.Add(value); | |
| } | |
| } | |
| // gather paths that need to be created | |
| for (ushort i = 1; i <= MAX_SPECIES_ID; i++) | |
| { | |
| var target = $"{i:0000}_00_00"; | |
| if (SpeciesBanlist.Contains((DevID)i)) | |
| continue; | |
| if (!GetIsPresentInGame(i, 0)) | |
| continue; | |
| if (presentPaths.Contains(target)) | |
| continue; | |
| missingPaths.Add(target); | |
| } | |
| presentPaths = presentPaths.Distinct().ToList(); | |
| missingPaths = missingPaths.Distinct().ToList(); | |
| // insert new paths into the table | |
| foreach (var t in missingPaths) | |
| { | |
| var value = obj.GetType().GetProperty($"Path{idx:000}"); | |
| value.SetValue(obj, t); | |
| idx++; | |
| } | |
| if (!Directory.Exists(dest)) | |
| Directory.CreateDirectory(dest); | |
| var data = FlatBufferConverter.SerializeFrom(obj); | |
| File.WriteAllBytes(Path.Combine(dest, file), data); | |
| } | |
| private static void AddNewBehaviorDataSingle() | |
| { | |
| var template = Path.Combine(PATH_MASTER, "world/data/pokemon/wild/wild_pokemon_single_data/0025_00_00.bfbs"); | |
| var dest = Path.Combine(DEST_MASTER, "world/data/pokemon/wild/wild_pokemon_single_data/"); | |
| if (!Directory.Exists(dest)) | |
| Directory.CreateDirectory(dest); | |
| foreach (var t in MissingBehaviorDataSingle) | |
| { | |
| var dev = ushort.Parse(t[0..4]); | |
| if (SpeciesBanlist.Contains((DevID)dev)) | |
| continue; | |
| var ground = PokemonMovementType.GROUND; | |
| var water = GetCanSwimOrBeAboveWater(dev, 0) && !GetCanFlyOrLevitate(dev, 0) ? PokemonMovementType.SWIMMING : PokemonMovementType.NONE; | |
| var sky = GetCanFlyOrLevitate(dev, 0) ? PokemonMovementType.FLYING : PokemonMovementType.NONE; | |
| var entry = new WildPokemonSingleData() | |
| { | |
| Movement = new MovementTypeData() { Ground = ground, Water = water, Sky = sky }, | |
| BasicAction = PrefilledBasicActionData, | |
| InstinctAction = PrefilledInstinctActionData, | |
| PopProbability = PrefilledPopProbabilityParam, | |
| WeatherReaction = new WeatherReactionData(), | |
| ObjectRecognition = PrefilledObjectRecognitionData, | |
| BattleReaction = PrefilledBattleActionData, | |
| PlayerEscapedReactionID = PokemonActionID.AFTER_BATTLE_REACTION, | |
| TreeShakingReactionID = PokemonActionID.JUMP_DOWN_ACTION_POINT_OCCUPY_RELEASE_STAN, | |
| HomeRange = 50.0f, | |
| AllowSuspend = false, | |
| BattleReactionID = PokemonActionID.EXCLUSION_AREA_ESCAPE_RUN_IF_BATTLE_MOVE_TO_NEARBY_BATTLE_HECKLER_AREA, | |
| EvolutionActionID = PokemonActionID.NONE, | |
| SoundRecognition = PrefilledSoundRecognitionData, | |
| }; | |
| var data = FlatBufferConverter.SerializeFrom(entry); | |
| File.WriteAllBytes(Path.Combine(dest, $"{t}.bin"), data); | |
| File.Copy(template, Path.Combine(dest, $"{t}.bfbs"), true); | |
| } | |
| } | |
| private static void AddNewBehaviorDataGroup() | |
| { | |
| var template = Path.Combine(PATH_MASTER, "world/data/pokemon/wild/wild_pokemon_group_data/0025_00_00.bfbs"); | |
| var dest = Path.Combine(DEST_MASTER, "world/data/pokemon/wild/wild_pokemon_group_data/"); | |
| if (!Directory.Exists(dest)) | |
| Directory.CreateDirectory(dest); | |
| foreach (var t in MissingBehaviorDataGroup) | |
| { | |
| var dev = ushort.Parse(t[0..4]); | |
| if (SpeciesBanlist.Contains((DevID)dev)) | |
| continue; | |
| var ground = PokemonMovementType.GROUND; | |
| var water = GetCanSwimOrBeAboveWater(dev, 0) && !GetCanFlyOrLevitate(dev, 0) ? PokemonMovementType.SWIMMING : PokemonMovementType.NONE; | |
| var sky = GetCanFlyOrLevitate(dev, 0) ? PokemonMovementType.FLYING : PokemonMovementType.NONE; | |
| var entry = new WildPokemonGroupData() | |
| { | |
| Movement = new MovementTypeData() { Ground = ground, Water = water, Sky = sky }, | |
| BasicAction = PrefilledBasicActionData, | |
| InstinctAction = PrefilledInstinctActionData, | |
| PopProbability = PrefilledPopProbabilityParam, | |
| WeatherReaction = new WeatherReactionData(), | |
| ObjectRecognition = PrefilledObjectRecognitionData, | |
| BattleReaction = PrefilledGroupBattleActionData, | |
| PlayerEscapedReactionID = PokemonActionID.AFTER_BATTLE_REACTION, | |
| TreeShakingReactionID = PokemonActionID.JUMP_DOWN_ACTION_POINT_OCCUPY_RELEASE_STAN, | |
| HomeRange = 50.0f, | |
| AllowSuspend = false, | |
| EvolutionActionID = PokemonActionID.NONE, | |
| SoundRecognition = PrefilledSoundRecognitionData, | |
| }; | |
| var data = FlatBufferConverter.SerializeFrom(entry); | |
| File.WriteAllBytes(Path.Combine(dest, $"{t}.bin"), data); | |
| File.Copy(template, Path.Combine(dest, $"{t}.bfbs"), true); | |
| } | |
| } | |
| private static void AddNewBehaviorDataSpecial() | |
| { | |
| var template = Path.Combine(PATH_MASTER, "world/data/pokemon/wild/wild_pokemon_special_data/0025_00_00.bfbs"); | |
| var dest = Path.Combine(DEST_MASTER, "world/data/pokemon/wild/wild_pokemon_special_data/"); | |
| if (!Directory.Exists(dest)) | |
| Directory.CreateDirectory(dest); | |
| foreach (var t in MissingBehaviorDataSpecial) | |
| { | |
| var dev = ushort.Parse(t[0..4]); | |
| if (SpeciesBanlist.Contains((DevID)dev)) | |
| continue; | |
| var ground = PokemonMovementType.GROUND; | |
| var water = GetCanSwimOrBeAboveWater(dev, 0) && !GetCanFlyOrLevitate(dev, 0) ? PokemonMovementType.SWIMMING : PokemonMovementType.NONE; | |
| var sky = GetCanFlyOrLevitate(dev, 0) ? PokemonMovementType.FLYING : PokemonMovementType.NONE; | |
| var entry = new WildPokemonSpecialData() | |
| { | |
| Movement = new MovementTypeData() { Ground = ground, Water = water, Sky = sky }, | |
| BasicAction = PrefilledBasicActionData, | |
| InstinctAction = PrefilledInstinctActionData, | |
| PopProbability = PrefilledPopProbabilityParam, | |
| WeatherReaction = new WeatherReactionData(), | |
| ObjectRecognition = PrefilledObjectRecognitionData, | |
| BattleReaction = PrefilledBattleActionData, | |
| PlayerEscapedReactionID = PokemonActionID.AFTER_BATTLE_REACTION, | |
| TreeShakingReactionID = PokemonActionID.JUMP_DOWN_ACTION_POINT_OCCUPY_RELEASE_STAN, | |
| HomeRange = 50.0f, | |
| AllowSuspend = false, | |
| IsRefereceSingleData = false, | |
| BattleReactionID = PokemonActionID.EXCLUSION_AREA_ESCAPE_RUN_IF_BATTLE_MOVE_TO_NEARBY_BATTLE_HECKLER_AREA, | |
| EvolutionActionID = PokemonActionID.NONE, | |
| SoundRecognition = PrefilledSoundRecognitionData, | |
| }; | |
| var data = FlatBufferConverter.SerializeFrom(entry); | |
| File.WriteAllBytes(Path.Combine(dest, $"{t}.bin"), data); | |
| File.Copy(template, Path.Combine(dest, $"{t}.bfbs"), true); | |
| } | |
| } | |
| private static void UpdatePokedexHabitatsMenu() | |
| { | |
| if (!DISPLAY_ABILITIES_IN_POKEDEX) | |
| return; | |
| "Updating Pokédex Habitats menu...".Dump(); | |
| IEnumerable<PersonalInfo> GetPersonalEntriesToIterate(int dex) => dex switch | |
| { | |
| 0 => PersonalInfoEdited.Table.Where(z => z.IsPresentInGame), | |
| 1 => PersonalInfoEdited.Table.Where(z => z.KitakamiDex != 0), | |
| 2 => PersonalInfoEdited.Table.Where(z => z.BlueberryDex != 0), | |
| _ => throw new ArgumentException(nameof(dex)), | |
| }; | |
| // set up custom hash table | |
| const int AHTB_MAGIC = 0x42544841; // AHTB | |
| for (int p = 0; p < 3; p++) | |
| { | |
| var personal = GetPersonalEntriesToIterate(p); | |
| for (int i = 0; i <= 1; i++) | |
| { | |
| var ver = i == 0 ? 'A' : 'B'; | |
| var entries = new List<AHTBEntry>(); | |
| foreach (var t in personal) | |
| { | |
| var dev = t.Info.SpeciesInternal; | |
| var form = t.Info.Form; | |
| for (int b = 1; b <= 3; b++) | |
| { | |
| var name = GetEntryName(p, ver, dev, form, b); | |
| var entry = new AHTBEntry(FnvHash.HashFnv1a_64(name), name); | |
| entries.Add(entry); | |
| } | |
| } | |
| var maxName = GetEntryNameMax(p, ver); | |
| var maxEntry = new AHTBEntry(FnvHash.HashFnv1a_64(maxName), maxName); | |
| entries.Add(maxEntry); | |
| foreach (var lang in LanguagesToIterate) | |
| { | |
| var file = GetFileName(p, ver); | |
| var dest = Path.Combine(DEST_MASTER, $"message/dat/{lang}/common/{file}.tbl"); | |
| using var ms = new MemoryStream(); | |
| { | |
| using var bw = new BinaryWriter(File.Open(dest, FileMode.Create)); | |
| { | |
| bw.Write(AHTB_MAGIC); | |
| bw.Write((uint)entries.Count); | |
| foreach (var t in entries) | |
| { | |
| bw.Write((ulong)t.Hash); | |
| bw.Write((ushort)(t.Name.Length + 1)); | |
| bw.Write(Encoding.UTF8.GetBytes(t.Name)); | |
| bw.Write((byte)0); | |
| } | |
| } | |
| } | |
| } | |
| } | |
| } | |
| // set up strings | |
| for (int p = 0; p < 3; p++) | |
| { | |
| var personal = GetPersonalEntriesToIterate(p); | |
| var fileA = GetFileName(p, 'A'); | |
| var fileB = GetFileName(p, 'B'); | |
| foreach (var lang in LanguagesToIterate) | |
| { | |
| List<string> Result = []; | |
| var dest = Path.Combine(DEST_MASTER, $"message/dat/{lang}/common/"); | |
| var abilities = GetStringsCommon(lang, "tokusei"); | |
| var species = GetStringsCommon(lang, "monsname"); | |
| var items = GetStringsCommon(lang, "itemname"); | |
| foreach (var t in personal) | |
| { | |
| // only 1 ability | |
| if (t.Ability1 == t.Ability2 && t.Ability1 == t.AbilityH) | |
| { | |
| var ability = IsAbilityVague(t.Ability1) ? GetDescriptiveAbilityName(t.Ability1, abilities, species, items) : abilities[t.Ability1]; | |
| Result.Add("\\n"); | |
| Result.Add(ability); | |
| Result.Add("\\n"); | |
| } | |
| else | |
| { | |
| var ability1 = IsAbilityVague(t.Ability1) ? GetDescriptiveAbilityName(t.Ability1, abilities, species, items) : abilities[t.Ability1]; | |
| var ability2 = IsAbilityVague(t.Ability2) ? GetDescriptiveAbilityName(t.Ability2, abilities, species, items) : abilities[t.Ability2]; | |
| var abilityH = IsAbilityVague(t.AbilityH) ? GetDescriptiveAbilityName(t.AbilityH, abilities, species, items) : abilities[t.AbilityH]; | |
| Result.Add(ability1 += " (1)"); | |
| Result.Add(ability2 += " (2)"); | |
| Result.Add(abilityH += " (H)"); | |
| } | |
| } | |
| var lines = Result.ToArray(); | |
| var flags = new ushort[lines.Length]; | |
| if (!Directory.Exists(dest)) | |
| Directory.CreateDirectory(dest); | |
| var write = TextFile.GetBytes(lines, flags, TextConfig); | |
| File.WriteAllBytes(Path.Combine(dest, $"{fileA}.dat"), write); | |
| File.WriteAllBytes(Path.Combine(dest, $"{fileB}.dat"), write); | |
| } | |
| } | |
| string GetEntryName(int dex, char ver, ushort dev, ushort form, int num) | |
| { | |
| var start = dex == 0 ? "BUNPU_COMMENT" : dex == 1 ? "BUNPU_COMMENT_DLC1" : "BUNPU_COMMENT_DLC2"; | |
| return $"{start}_{ver}_{dev:000}_{form:000}_{num:00}"; | |
| } | |
| string GetEntryNameMax(int dex, char ver) | |
| { | |
| var start = dex == 0 ? "msg_bunpu_comment" : dex == 1 ? "msg_bunpu_comment_dlc1" : "msg_bunpu_comment_dlc2"; | |
| return $"{start}_{ver}_max"; | |
| } | |
| string GetFileName(int dex, char ver) | |
| { | |
| var start = dex == 0 ? "bunpu_comment" : dex == 1 ? "bunpu_comment_dlc1" : "bunpu_comment_dlc2"; | |
| return $"{start}_{ver}"; | |
| } | |
| UpdatePokedexDistributionData(); | |
| } | |
| private static void UpdatePokedexDistributionData() | |
| { | |
| var distributionData = new[] | |
| { | |
| "world/data/ui/pokedex/distribution_data/distribution_data_array.bin", | |
| "world/data/ui/pokedex/distribution_data_dlc1/distribution_data_dlc1_array.bin", | |
| "world/data/ui/pokedex/distribution_data_dlc2/distribution_data_dlc2_array.bin", | |
| }; | |
| IEnumerable<PersonalInfo> GetPersonalEntriesToIterate(int dex) => dex switch | |
| { | |
| 0 => PersonalInfoEdited.Table.Where(z => z.IsPresentInGame), | |
| 1 => PersonalInfoEdited.Table.Where(z => z.KitakamiDex != 0), | |
| 2 => PersonalInfoEdited.Table.Where(z => z.BlueberryDex != 0), | |
| _ => throw new ArgumentException(nameof(dex)), | |
| }; | |
| var grid = new GridParam() { X = 999, Y = 999 }; // only draw 1 tile on the grid, but keep it off screen | |
| foreach (var t in distributionData) | |
| { | |
| var index = t.LastIndexOf("/") + 1; | |
| var f = t[index..]; | |
| var d = t[..index]; | |
| var file = Path.Combine(PATH_MASTER, d, f); | |
| var obj = FlatBufferConverter.DeserializeFrom<DistributionRootArray>(file); | |
| var dex = distributionData.IndexOf(t); | |
| var personal = GetPersonalEntriesToIterate(dex); | |
| obj.Table.Clear(); | |
| foreach (var p in personal) | |
| { | |
| var dev = p.Info.SpeciesInternal; | |
| var form = p.Info.Form; | |
| var dist = new DistributionData() | |
| { | |
| DevNo = dev, | |
| FormNo = form, | |
| VersionA = true, | |
| VersionB = true, | |
| Grid = grid, | |
| }; | |
| var dataTable = new List<DistributionData> { dist }; | |
| var rootTable = new DistributionRoot { Table = dataTable }; | |
| obj.Table.Add(rootTable); | |
| } | |
| var data = FlatBufferConverter.SerializeFrom(obj); | |
| var dest = Path.Combine(DEST_MASTER, d); | |
| if (!Directory.Exists(dest)) | |
| Directory.CreateDirectory(dest); | |
| File.WriteAllBytes(Path.Combine(dest, f), data); | |
| } | |
| } | |
| private static void ApplyExtraChanges() | |
| { | |
| if (RANDOMIZE_ROAMING_FORM_GIMMIGHOUL) | |
| { | |
| var gimmighoul = GenerateFakeScenePokemon(); | |
| UpdateOverworldSceneSingle("world/obj_template/parts/coin_symbol/coin_symbol_walk_/coin_symbol_walk_{0}.trsot", gimmighoul); | |
| } | |
| if (RANDOMIZE_POKEMON_LEARNSETS == LearnsetRandomizer.MetronomeOnly) | |
| { | |
| MoveInfoEdited.Table.FirstOrDefault(z => z.MoveID == 118).PP = 40; | |
| UpdateMoveTable(); | |
| } | |
| if (RANDOMIZE_GENERIC_OVERWORLD_POKEMON) | |
| { | |
| foreach (var t in GenericOverworldPokemonScenes) | |
| UpdateOverworldSceneGeneric(t); | |
| // Ditto Blocks | |
| var ditto = GenerateFakeScenePokemon(); | |
| var r = (ditto.Species, ditto.Form, ditto.Sex, RareType.NO_RARE); | |
| var s = (ditto.Species, ditto.Form, ditto.Sex, RareType.RARE); | |
| UpdateOverworldSceneSingle("world/obj_template/field/cubeparts_2/objects_un_dlc2_normal_poke_objtemp_/objects_un_dlc2_normal_poke_objtemp_{0}.trsot", r); | |
| UpdateOverworldSceneSingle("world/obj_template/field/cubeparts_2/objects_un_dlc2_rare_poke_objtemp_/objects_un_dlc2_rare_poke_objtemp_{0}.trsot", s); | |
| RandomizeNPCTraffic(); | |
| } | |
| if (RANDOMIZE_TRAINER_NAMES && LANGUAGE == "English") | |
| UpdateTrainerNames(); | |
| if (RANDOMIZE_TRAINER_CLASSES && LANGUAGE == "English") | |
| UpdateTrainerClasses(); | |
| } | |
| private static void RandomizeNPCTraffic() | |
| { | |
| const string file = "npc_traffic_generate_array.bin"; | |
| var dest = Path.Combine(DEST_MASTER, "world/data/event/npc_traffic_generate/"); | |
| var path = Path.Combine(PATH_MASTER, "world/data/event/npc_traffic_generate/"); | |
| var obj = FlatBufferConverter.DeserializeFrom<NpcTrafficGenerateTableArray>(Path.Combine(path, file)); | |
| foreach (var t in obj.Table) | |
| { | |
| var traffic = new[] | |
| { | |
| t.Data1, t.Data2, t.Data3, t.Data4, t.Data5, t.Data6, t.Data7, t.Data8, t.Data9, t.Data10, | |
| t.Data11, t.Data12, t.Data13, t.Data14, t.Data15, t.Data16, t.Data17, t.Data18, t.Data19, t.Data20, | |
| }; | |
| foreach (var npc in traffic.Where(z => z is { PartnerPkparam.DevId: not 0 })) | |
| { | |
| var pk = npc.PartnerPkparam; | |
| pk.DevId = GetRandomSpecies(pk.DevId); | |
| pk.FormId = GetRandomForm(pk.DevId); | |
| pk.Sex = GetRandomGender(pk.DevId, pk.FormId); | |
| pk.IsRare = GetRandom(100) < GENERIC_OVERWORLD_POKEMON_SHINY_CHANCE_PERCENT; | |
| pk.Scale = GetRandom(256); | |
| } | |
| } | |
| if (!Directory.Exists(dest)) | |
| Directory.CreateDirectory(dest); | |
| var data = FlatBufferConverter.SerializeFrom(obj); | |
| File.WriteAllBytes(Path.Combine(dest, file), data); | |
| } | |
| private static void UpdateTrainerNames() | |
| { | |
| "Updating trainer names...".Dump(); | |
| List<string> TrainerNames = | |
| [ | |
| // Universal Pokémon Randomizer | |
| "Alex", "Ann", "Anne", "Antoinette", "Bob", "Cammie", "Chloe", "Christopher", "Cristiano", "Crystal", | |
| "Daniel", "Donna", "Dudley", "Esteban", "Florentino", "Forrest", "Jacqueline", "Jasmine", "Jessica", | |
| "Jill", "Jimmy", "Jo", "Joe", "Joey", "Jonathan", "Kate", "Kim", "Kimberley", "Kristine", "Landon", | |
| "Lawrence", "Leo", "Lillianna", "Malcolm", "Mallorie", "Marcellus", "Marilyn", "Mark", "Marshall", | |
| "Max", "Maximillian", "Mia", "Mike", "Noelle", "Pansy", "Rachel", "Robin", "Scarlett", "Scott", | |
| "Sonya", "Stacey", "Thomas", "Tia", "Tristian", "Veronica", "Wayne", "Webster", "Wellington", "Zoey", | |
| // Titan Randomizer | |
| "Cleo", "Cody", "Donald", "Freddie", "Jade", "Josh", "Kurt", "Liam", "Marcus", "Matthew", | |
| "Mel", "Michael", "Natalie", "Patrick", "Peter", "Tim", "Vincent", "Vivian", "Volo", "Zack", | |
| ]; | |
| List<string> CustomTrainerNames = []; | |
| if (!File.Exists(PATH_TEXT_FILE_CUSTOM_TRAINER_NAMES)) | |
| { | |
| "ERROR: Specified file for custom trainer names does not exist!".Dump(); | |
| } | |
| else | |
| { | |
| var names = File.ReadAllLines(PATH_TEXT_FILE_CUSTOM_TRAINER_NAMES); | |
| if (names.Count() == 0) | |
| "ERROR: Trainer names text file is empty!".Dump(); | |
| foreach (var name in names) | |
| { | |
| // blank line | |
| if (name.Length == 0) | |
| continue; | |
| // replace common apostrophe with Game Freak apostrophe | |
| var revised = name.Replace("'", "’"); | |
| // duplicate | |
| if (TrainerNames.Contains(revised) || CustomTrainerNames.Contains(revised)) | |
| continue; | |
| // exceeds character limit | |
| if (revised.Length > 50) | |
| { | |
| $"ERROR: Failed to add trainer name, exceeds character limit of 50: {revised}".Dump(); | |
| continue; | |
| } | |
| CustomTrainerNames.Add(revised); | |
| } | |
| } | |
| var trainerNamesToUse = ONLY_USE_CUSTOM_TRAINER_NAMES ? CustomTrainerNames : TrainerNames.Concat(CustomTrainerNames).ToList(); | |
| var dest = Path.Combine(DEST_MASTER, "message/dat/English/common/"); | |
| var lines = GetStringsCommon(LANGUAGE, "trname"); | |
| var flags = GetStringFlagsCommon(LANGUAGE, "trname"); | |
| for (int i = 1; i < lines.Length; i++) | |
| { | |
| if (string.IsNullOrWhiteSpace(lines[i]) || lines[i].StartsWith("[~ ")) | |
| continue; | |
| var name = trainerNamesToUse[GetRandom(trainerNamesToUse.Count)]; | |
| // special handling to always refer to AI Sada/AI Turo as AI {name} | |
| if (i is 18 or 19) | |
| { | |
| name = $"AI {name}"; | |
| flags[i] |= (1 << 2); | |
| } | |
| lines[i] = name; | |
| flags[i] = 0; | |
| if (GetStartsWithVowel(name)) | |
| flags[i] |= (1 << 2); | |
| } | |
| if (!Directory.Exists(dest)) | |
| Directory.CreateDirectory(dest); | |
| var write = TextFile.GetBytes(lines, flags, TextConfig); | |
| File.WriteAllBytes(Path.Combine(dest, "trname.dat"), write); | |
| } | |
| private static void UpdateTrainerClasses() | |
| { | |
| "Updating trainer classes...".Dump(); | |
| List<(string Name, string Format)> TrainerClasses = | |
| [ | |
| // Vanilla | |
| ( "AI", "" ), ( "Artist", "the" ), ( "BB League Champion", "" ), ( "BB League Elite Four", "of the" ), ( "Backpacker", "" ), ( "Black Belt", "the" ), | |
| ( "Blueberry Academy Director", "" ), ( "Cabbie", "" ), ( "Champion", "" ), ( "Cook", "the" ), ( "Courier", "the" ), ( "Delinquent", "the" ), | |
| ( "Director", "" ), ( "Dragon Tamer", "the" ), ( "Elite Four", "of the" ), ( "Festival Boy", "the" ), ( "Festival Girl", "the" ), | |
| ( "Glitterati", "of the" ), ( "Grandma", "" ), ( "Grandpa", "" ), ( "Gym Leader", "" ), ( "Gym Trainer", "" ), ( "Hiker", "" ), | |
| ( "Instructor", "the" ), ( "Janitor", "the" ), ( "Kitakami Ogre Clan", "of the" ), ( "League Club", "of the" ), ( "Model", "the" ), | |
| ( "Musician", "the" ), ( "Office Worker", "the" ), ( "Photographer", "the" ), ( "Poké Maniac", "the" ), ( "Pokémon Trainer", "" ), | |
| ( "Ruffian", "the" ), ( "Scientist", "the" ), ( "Student", "the" ), ( "Team Star", "of" ), ( "Top Champion", "" ), ( "Waiter", "the" ), | |
| ( "Waitress", "the" ), ( "Worker", "the" ), ( "Youngster", "" ), | |
| // Universal Pokémon Randomizer | |
| ( "Ace Trainer", "" ), ( "Analyst", "" ), ( "Artisan", "the" ), ( "Author", "the" ), ( "Biker", "the" ), ( "Boss", "the" ), ( "Botanist", "the" ), | |
| ( "Bug Catcher", "the" ), ( "Chef", "" ), ( "Chief", "the" ), ( "Clerk", "the" ), ( "Cool Trainer", "" ), ( "Cop", "the" ), ( "Cyclist", "the" ), | |
| ( "Dancer", "the" ), ( "Designer", "the" ), ( "Developer", "the" ), ( "Dr.", "" ), ( "Elite Trainer", "" ), ( "Engineer", "the" ), ( "Entrepreneur", "the" ), | |
| ( "Fisher", "the" ), ( "Ghostwriter", "the" ), ( "God", "the" ), ( "Gymnast", "the" ), ( "Juggler", "the" ), ( "Lazy", "" ), ( "Manager", "the" ), ( "Nurse", "" ), | |
| ( "Operator", "the" ), ( "Performer", "the" ), ( "Pokémon Breeder", "" ), ( "Poet", "the" ), ( "Preschooler", "the" ), ( "Ranger", "" ), ( "Rich Kid", "the" ), | |
| ( "Rocker", "the" ), ( "Sailor", "the" ), ( "School Kid", "the" ), ( "Scribe", "the" ), ( "Servant", "the" ), ( "Swimmer", "the" ), ( "Tamer", "the" ), | |
| ( "Technician", "the" ), ( "Weightlifter", "the" ), ( "Writer", "the" ), | |
| // Titan Randomizer | |
| ( "Babysitter", "the" ), ( "Cat Lover", "the" ), ( "Dog Lover", "the" ), ( "GAME FREAK’s", "" ), ( "Gamer", "the" ), ( "ILCA’s", "" ), ( "Landlord", "the" ), ( "Lawyer", "the" ), | |
| ( "Prankster", "the" ), ( "President", "" ), ( "Professional", "the" ), ( "Racer", "the" ), ( "Realtor", "the" ), ( "Speedrunner", "the" ), ( "Streamer", "the" ), | |
| ]; | |
| List<(string Name, string Format)> CustomTrainerClasses = []; | |
| if (!File.Exists(PATH_TEXT_FILE_CUSTOM_TRAINER_CLASSES)) | |
| { | |
| "ERROR: Specified file for custom trainer classes does not exist!".Dump(); | |
| } | |
| else | |
| { | |
| var names = File.ReadAllLines(PATH_TEXT_FILE_CUSTOM_TRAINER_CLASSES); | |
| if (names.Count() == 0) | |
| "ERROR: Trainer classes text file is empty!".Dump(); | |
| foreach (var line in names) | |
| { | |
| // blank line | |
| if (line.Length == 0) | |
| continue; | |
| var arr = line.Split('\t'); | |
| var name = arr[0].Replace("'", "’"); | |
| var format = arr[1]; | |
| // duplicate | |
| if (TrainerClasses.Select(z => z.Name).Contains(name)) | |
| continue; | |
| // exceeds character limit | |
| if (name.Length > 50) | |
| { | |
| $"ERROR: Failed to add trainer class, exceeds character limit of 50: {name}".Dump(); | |
| continue; | |
| } | |
| CustomTrainerClasses.Add((name, format)); | |
| } | |
| } | |
| var trainerClassesToUse = ONLY_USE_CUSTOM_TRAINER_CLASSES ? CustomTrainerClasses : TrainerClasses.Concat(CustomTrainerClasses).ToList(); | |
| var dest = Path.Combine(DEST_MASTER, "message/dat/English/common/"); | |
| var lines = GetStringsCommon(LANGUAGE, "trtype"); | |
| var flags = GetStringFlagsCommon(LANGUAGE, "trtype"); | |
| for (int i = 0; i < lines.Length; i++) | |
| { | |
| if (string.IsNullOrWhiteSpace(lines[i]) || lines[i].StartsWith("[~ ")) | |
| continue; | |
| var trClass = trainerClassesToUse[GetRandom(trainerClassesToUse.Count)]; | |
| var name = trClass.Name; | |
| var format = trClass.Format; | |
| var trFlags = GetTrainerClassFlags(name, format); | |
| lines[i] = name; | |
| flags[i] = trFlags; | |
| } | |
| if (!Directory.Exists(dest)) | |
| Directory.CreateDirectory(dest); | |
| var write = TextFile.GetBytes(lines, flags, TextConfig); | |
| File.WriteAllBytes(Path.Combine(dest, "trtype.dat"), write); | |
| } | |
| private static ushort GetTrainerClassFlags(string type, string format) | |
| { | |
| const byte POS_GRAMMAR_GENDER = 2; | |
| const byte POS_FORMAT = 7; | |
| const byte POS_ARTICLE = 9; | |
| ushort flags = 0; | |
| if (GetStartsWithVowel(type)) | |
| flags |= (1 << POS_GRAMMAR_GENDER); | |
| if (format == "of the") | |
| flags |= (1 << POS_FORMAT); | |
| if (format == "of") | |
| { | |
| flags |= (1 << POS_FORMAT); | |
| flags |= (1 << POS_ARTICLE); | |
| } | |
| if (format == "the") | |
| { | |
| flags |= (2 << POS_FORMAT); | |
| flags |= (1 << POS_ARTICLE); | |
| } | |
| return flags; | |
| } | |
| private static void UpdateTextSimpleSpecies(string type, string file, int[] devOld, DevID[] devNew, int[] indices) | |
| { | |
| bool IsGrammarCheck(string file) => file is "gym_kusa_020" or "kizuna_language_d" or "popup" or "s1_side02_0060" or "sub_001"; | |
| bool IsModifySpeciesVariable(string file) => file is "atlantis_talk_05" or "common_0270" or "common_1100"; | |
| const string NAME_VARIABLE_NORMAL = "[VAR 0101(0001)]"; | |
| const string NAME_VARIABLE_IGUANA = "[VAR 1107(00FF,0202)]KoMiraidon"; | |
| foreach (var lang in LanguagesToIterate) | |
| { | |
| var dest = Path.Combine(DEST_MASTER, $"message/dat/{lang}/{type}/"); | |
| var names = GetStringsCommon(lang, "monsname"); | |
| var lines = type == "common" ? GetStringsCommon(lang, file) : GetStringsScript(lang, file); | |
| var flags = type == "common" ? GetStringFlagsCommon(lang, file) : GetStringFlagsScript(lang, file); | |
| var ctr = 0; | |
| foreach (var i in indices) | |
| { | |
| var nameOld = devOld.Length == 1 ? names[devOld[0]] : names[devOld[ctr]]; | |
| var nameNew = devNew.Length == 1 ? names[(ushort)devNew[0]] : names[(ushort)devNew[ctr]]; | |
| var temp = $"TEMP_NAME_POKEMON_{ctr}"; | |
| // grammar | |
| if (lang == "English" && IsGrammarCheck(file) && GetStartsWithVowel(nameNew)) | |
| { | |
| if (lines[i].Contains($"A {nameOld}")) | |
| lines[i] = lines[i].Replace($"A {nameOld}", $"An {nameNew}"); | |
| else if (lines[i].Contains($"a {nameOld}")) | |
| lines[i] = lines[i].Replace($"a {nameOld}", $"an {nameNew}"); | |
| } | |
| // not an exact match | |
| if (lang == "Spanish" && file == "popup" && i == 1511) | |
| lines[i] = lines[i].Replace("Pincurchiiiin", nameNew); | |
| if (lang is "English" or "French" or "German" or "Italian" or "Spanish" && file == "s1_side02_0050" && i == 28) | |
| { | |
| var stutterOld = lang switch | |
| { | |
| "French" => $"{nameOld[..2]}... ", | |
| "Italian" => $"{nameOld[..3]}... ", | |
| _ => $"{nameOld[0]}-", | |
| }; | |
| var stutterNew = lang switch | |
| { | |
| "French" => $"{nameNew[..2]}... ", | |
| "Italian" => $"{nameNew[..3]}... ", | |
| _ => $"{nameNew[0]}-", | |
| }; | |
| var replaceOld = lang == "Spanish" ? nameOld : $"{stutterOld}{nameOld}"; | |
| var replaceNew = lang == "Spanish" ? nameNew : $"{stutterNew}{nameNew}"; | |
| lines[i] = lines[i].Replace(replaceOld, replaceNew); | |
| } | |
| else | |
| lines[i] = lines[i].Replace(nameOld, temp); | |
| ctr++; | |
| } | |
| if (lines.Any(z => z.Contains("TEMP_NAME_POKEMON"))) | |
| { | |
| ctr = 0; | |
| foreach (var i in indices) | |
| { | |
| var nameNew = devNew.Length == 1 ? names[(ushort)devNew[0]] : names[(ushort)devNew[ctr]]; | |
| var temp = $"TEMP_NAME_POKEMON_{ctr}"; | |
| if (!lines[i].Contains(temp)) | |
| { | |
| ctr++; | |
| continue; | |
| } | |
| lines[i] = lines[i].Replace(temp, nameNew); | |
| ctr++; | |
| } | |
| } | |
| if (IsModifySpeciesVariable(file)) | |
| { | |
| int[] extraIndices = file switch | |
| { | |
| "atlantis_talk_05" => [2,12,22], | |
| "common_0270" => [15,16,19,20,57], | |
| "common_1100" => [14,16,66,67,69], | |
| _ => throw new ArgumentException(nameof(file), file), | |
| }; | |
| foreach (var i in extraIndices) | |
| { | |
| var nameNew = names[(ushort)devNew[0]]; | |
| lines[i] = lines[i].Replace(NAME_VARIABLE_NORMAL, nameNew); | |
| } | |
| } | |
| if (file is "atlantis_talk_05" or "sdc01_side01_010") | |
| { | |
| var indexLine = file == "atlantis_talk_05" ? 21 : 35; | |
| var indexDev = file == "atlantis_talk_05" ? 0 : 3; | |
| lines[indexLine] = lines[indexLine].Replace(NAME_VARIABLE_IGUANA, names[(ushort)devNew[indexDev]]); | |
| } | |
| if (!Directory.Exists(dest)) | |
| Directory.CreateDirectory(dest); | |
| var write = TextFile.GetBytes(lines, flags, TextConfig); | |
| File.WriteAllBytes(Path.Combine(dest, $"{file}.dat"), write); | |
| } | |
| } | |
| private static void UpdateTextVariableSpecies(string type, string file, DevID dev, int[] indices) | |
| { | |
| const string NAME_OLD = "[VAR 0101(0001)]"; | |
| foreach (var lang in LanguagesToIterate) | |
| { | |
| var dest = Path.Combine(DEST_MASTER, $"message/dat/{lang}/{type}/"); | |
| var names = GetStringsCommon(lang, "monsname"); | |
| var lines = type == "common" ? GetStringsCommon(lang, file) : GetStringsScript(lang, file); | |
| var flags = type == "common" ? GetStringFlagsCommon(lang, file) : GetStringFlagsScript(lang, file); | |
| var nameNew = names[(ushort)dev]; | |
| foreach (var i in indices) | |
| lines[i] = lines[i].Replace(NAME_OLD, nameNew); | |
| if (!Directory.Exists(dest)) | |
| Directory.CreateDirectory(dest); | |
| var write = TextFile.GetBytes(lines, flags, TextConfig); | |
| File.WriteAllBytes(Path.Combine(dest, $"{file}.dat"), write); | |
| } | |
| } | |
| private static List<DevID> GetSpeciesBanlist() | |
| { | |
| List<DevID> Banned = []; | |
| if (!ALLOW_SPECIES_GEN_1) Banned.AddRange(Enumerable.Range(0001, 151).Select(z => (DevID)z)); | |
| if (!ALLOW_SPECIES_GEN_2) Banned.AddRange(Enumerable.Range(0152, 100).Select(z => (DevID)z)); | |
| if (!ALLOW_SPECIES_GEN_3) Banned.AddRange(Enumerable.Range(0252, 135).Select(z => (DevID)z)); | |
| if (!ALLOW_SPECIES_GEN_4) Banned.AddRange(Enumerable.Range(0387, 107).Select(z => (DevID)z)); | |
| if (!ALLOW_SPECIES_GEN_5) Banned.AddRange(Enumerable.Range(0494, 156).Select(z => (DevID)z)); | |
| if (!ALLOW_SPECIES_GEN_6) Banned.AddRange(Enumerable.Range(0650, 072).Select(z => (DevID)z)); | |
| if (!ALLOW_SPECIES_GEN_7) Banned.AddRange(Enumerable.Range(0722, 088).Select(z => (DevID)z)); | |
| if (!ALLOW_SPECIES_GEN_8) Banned.AddRange(Enumerable.Range(0810, 096).Select(z => (DevID)z)); | |
| if (!ALLOW_SPECIES_GEN_9) Banned.AddRange(Enumerable.Range(0906, 120).Select(z => (DevID)z)); | |
| // failsafe if we ban all species | |
| if (Banned.Count == MAX_SPECIES_ID) | |
| Banned.Clear(); | |
| foreach (var t in PersonalInfoVanilla.Table.Where(z => !z.IsPresentInGame && z.Info.Form == 0)) | |
| Banned.Add((DevID)t.Info.SpeciesInternal); | |
| if (!ALLOW_SPECIES_LEGENDARY) Banned.AddRange(LegendaryPokemon.Select(z => (DevID)GetDevID(z))); | |
| if (!ALLOW_SPECIES_MYTHICAL) Banned.AddRange(MythicalPokemon.Select(z => (DevID)GetDevID(z))); | |
| if (!ALLOW_SPECIES_PARADOX) Banned.AddRange(ParadoxPokemon.Select(z => (DevID)GetDevID(z))); | |
| Banned = Banned.Distinct().OrderBy(z => z).ToList(); | |
| return Banned; | |
| } | |
| private static List<int> GetAbilityBanlist() | |
| { | |
| List<int> Banned = | |
| [ | |
| 059, // Forecast | |
| 121, // Multitype | |
| 122, // Flower Gift | |
| 161, // Zen Mode | |
| 176, // Stance Change | |
| 193, // Wimp Out | |
| 194, // Emergency Exit | |
| 197, // Shields Down | |
| 208, // Schooling | |
| 209, // Disguise | |
| 210, // Battle Bond | |
| 211, // Power Construct | |
| 225, // RKS System | |
| 241, // Gulp Missile | |
| 248, // Ice Face | |
| 258, // Hunger Switch | |
| 278, // Zero to Hero | |
| 279, // Commander | |
| 301, // Embody Aspect (Teal Mask) | |
| 302, // Embody Aspect (Hearthflame Mask) | |
| 303, // Embody Aspect (Wellspring Mask) | |
| 304, // Embody Aspect (Cornerstone Mask) | |
| 307, // Tera Shift | |
| 308, // Tera Shell | |
| 309, // Teraform Zero | |
| 310, // Poison Puppeteer | |
| ]; | |
| List<int> DoublesOnly = | |
| [ | |
| 057, // Plus | |
| 058, // Minus | |
| 131, // Healer | |
| 132, // Friend Guard | |
| 140, // Telepathy | |
| 180, // Symbiosis | |
| 217, // Battery | |
| 222, // Receiver | |
| 223, // Power of Alchemy | |
| 249, // Power Spot | |
| 261, // Curious Medicine | |
| 294, // Costar | |
| 299, // Hospitality | |
| ]; | |
| List<int> Trapping = | |
| [ | |
| 023, // Shadow Tag | |
| 042, // Magnet Pull | |
| 071, // Arena Trap | |
| ]; | |
| List<int> DuplicateEffect = | |
| [ | |
| 058, // Minus (Plus) | |
| 072, // Vital Spirit (Insomnia) | |
| 073, // White Smoke (Clear Body) | |
| 074, // Pure Power (Huge Power) | |
| 075, // Shell Armor (Battle Armor) | |
| 076, // Air Lock (Cloud Nine) | |
| 116, // Solid Rock (Filter) | |
| 152, // Mummy (Lingering Aroma) | |
| 160, // Iron Barbs (Rough Skin) | |
| 163, // Turboblaze (Mold Breaker) | |
| 164, // Teravolt (Mold Breaker) | |
| 219, // Dazzling (Queenly Majesty) | |
| 221, // Tangling Hair (Gooey) | |
| 223, // Power of Alchemy (Receiver) | |
| 230, // Full Metal Body (Clear Body) | |
| 231, // Shadow Shield (Multiscale) | |
| 236, // Libero (Protean) | |
| 242, // Stalwart (Propeller Tail) | |
| 264, // Chilling Neigh (Moxie) | |
| 296, // Armor Tail (Queenly Majesty) | |
| ]; | |
| if (BAN_ABILITY_WONDER_GUARD) | |
| Banned.Add(025); | |
| if (BAN_DOUBLE_BATTLE_ABILITIES) | |
| Banned.AddRange(DoublesOnly.Select(z => z)); | |
| if (BAN_TRAPPING_ABILITIES) | |
| Banned.AddRange(Trapping.Select(z => z)); | |
| if (COMBINE_DUPLICATE_ABILITIES) | |
| Banned.AddRange(DuplicateEffect.Select(z => z)); | |
| Banned = Banned.Distinct().OrderBy(z => z).ToList(); | |
| return Banned; | |
| } | |
| private static List<DevID> GetSpeciesList() => Enumerable.Range(1, MAX_SPECIES_ID).Select(z => (DevID)z).Except(SpeciesBanlist).ToList(); | |
| private static List<ushort> GetLegendaryList() => new() | |
| { | |
| 0144, 0145, 0146, 0150, | |
| 0243, 0244, 0245, 0249, 0250, | |
| 0377, 0378, 0379, 0380, 0381, 0382, 0383, 0384, | |
| 0480, 0481, 0482, 0483, 0484, 0485, 0486, 0487, 0488, | |
| 0638, 0639, 0640, 0641, 0642, 0643, 0644, 0645, 0646, | |
| 0716, 0717, 0718, | |
| 0772, 0773, 0785, 0786, 0787, 0788, 0789, 0790, 0791, 0792, 0800, | |
| 0888, 0889, 0890, 0891, 0892, 0894, 0895, 0896, 0897, 0898, 0905, | |
| 1001, 1002, 1003, 1004, 1007, 1008, 1014, 1015, 1016, 1017, 1024, | |
| }; | |
| private static List<ushort> GetMythicalList() => new() | |
| { | |
| 0151, | |
| 0251, | |
| 0385, 0386, | |
| 0489, 0490, 0491, 0492, 0493, | |
| 0494, 0647, 0648, 0649, | |
| 0719, 0720, 0721, | |
| 0801, 0802, 0807, 0808, 0809, | |
| 0893, | |
| 1025, | |
| }; | |
| private static List<ushort> GetParadoxList() => new() | |
| { | |
| 0984, 0985, 0986, 0987, 0988, 0989, | |
| 0990, 0991, 0992, 0993, 0994, 0995, | |
| 1005, 1006, | |
| 1009, 1010, | |
| 1020, 1021, 1022, 1023, | |
| }; | |
| private static List<ushort> GetSpecialPokemonList() => LegendaryPokemon.Concat(MythicalPokemon).Concat(ParadoxPokemon).ToList(); | |
| private static List<int> GetPermittedAbilities() => Enumerable.Range(1, MAX_ABILITY_ID).Except(AbilityBanlist).ToList(); | |
| private static List<ushort> GetItemList() | |
| { | |
| var file = Path.Combine(PATH_MASTER, "world/data/item/itemdata/itemdata_array.bin"); | |
| var obj = FlatBufferConverter.DeserializeFrom<ItemDataArray>(file); | |
| List<FieldPocket> DisallowedPockets = [ FieldPocket.FPOCKET_WAZA, FieldPocket.FPOCKET_EVENT, FieldPocket.FPOCKET_MATERIAL ]; | |
| // despite having item data, info strings, and icons, the game has error handling that can display them as "None" when received | |
| // the game does not have any error handling if they're found as field items, but they eventually get removed from the bag anyway | |
| List<int> DisallowedItems = | |
| [ | |
| 0016, // Cherish Ball | |
| 0111, // Odd Keystone | |
| 0224, // Cleanse Tag | |
| 0485, // Red Apricorn | |
| 0486, // Blue Apricorn | |
| 0487, // Yellow Apricorn | |
| 0488, // Green Apricorn | |
| 0489, // Pink Apricorn | |
| 0490, // White Apricorn | |
| 0491, // Black Apricorn | |
| 0500, // Park Ball | |
| 0708, // Lumiose Galette | |
| 0709, // Shalour Sable | |
| 1785, // Strange Ball | |
| 1888, // Baguette | |
| ]; | |
| if (!ALLOW_RANDOM_PICNIC_ITEMS) | |
| { | |
| DisallowedPockets.Add(FieldPocket.FPOCKET_PICNIC); | |
| } | |
| // we only want picks and ingredients in the pool; accessories should be capped at 1 | |
| else | |
| { | |
| for (int i = 1947; i <= 1955; i++) DisallowedItems.Add(i); // unused | |
| for (int i = 2311; i <= 2333; i++) DisallowedItems.Add(i); // Bottles, Cups, Tablecloths, Balls, Picnic Set | |
| for (int i = 2348; i <= 2384; i++) DisallowedItems.Add(i); // Bottles, Cups, Tablecloths, Balls | |
| for (int i = 2395; i <= 2400; i++) DisallowedItems.Add(i); // Dishes | |
| for (int i = 2417; i <= 2437; i++) DisallowedItems.Add(i); // Chairs | |
| for (int i = 2551; i <= 2552; i++) DisallowedItems.Add(i); // Blueberry Tablecloth, Blueberry Chairs | |
| } | |
| var items = obj.Table.Where(z => !DisallowedPockets.Contains(z.FieldPocket) && !DisallowedItems.Contains(z.Id)).Select(z => (ushort)z.Id).ToList(); | |
| items.Add(2351); // Black Augurite | |
| items.Add(2352); // Peat Block | |
| items.Add(2353); // Foreign Treat | |
| items.Add(2354); // Linking Cord | |
| items = items.Distinct().OrderBy(z => z).ToList(); | |
| return items; | |
| } | |
| private static List<int> GetTMItemIndices() | |
| { | |
| var file = Path.Combine(PATH_MASTER, "world/data/item/itemdata/itemdata_array.bin"); | |
| var obj = FlatBufferConverter.DeserializeFrom<ItemDataArray>(file); | |
| return obj.Table.Where(z => z.FieldPocket == FieldPocket.FPOCKET_WAZA).Select(z => z.Id).ToList(); | |
| } | |
| private static List<ItemID> GetSortedMaterialList() | |
| { | |
| var file = Path.Combine(PATH_MASTER, "world/data/item/dropitemdata/dropitemdata_array.bin"); | |
| var obj = FlatBufferConverter.DeserializeFrom<DropItemDataArray>(file); | |
| var items = obj.Table.Where(z => z.Item1.ItemId is not (ItemID.ITEMID_NONE or ItemID.ITEMID_SOZAI182)).Select(z => z.Item1.ItemId).Distinct(); | |
| Dictionary<ItemID, string> Ordered = []; | |
| foreach (var t in items) | |
| Ordered.Add(t, StringsItem[(ushort)t]); | |
| Ordered = Ordered.OrderBy(z => z.Value).ToDictionary(); | |
| return Ordered.Select(z => z.Key).ToList(); | |
| } | |
| private static List<int> GetPermittedMoves() | |
| { | |
| if (ALLOW_ALL_UNUSABLE_MOVES_TO_BE_USED) | |
| { | |
| foreach (var t in MoveInfoEdited.Table) | |
| t.CanUseMove = true; | |
| UpdateMoveTable(); | |
| } | |
| var moves = MoveInfoEdited.Table.Where(z => z.CanUseMove).Select(z => (int)z.MoveID).ToList(); | |
| return moves; | |
| } | |
| private static DevID GetRandomSpecies(DevID dev) | |
| { | |
| var species = GetNationalDexID((ushort)dev); | |
| var rnd = SpeciesList[GetRandom(SpeciesList.Count)]; | |
| bool swap = (FORCE_SWAP_SPECIES_LEGENDARY && ALLOW_SPECIES_LEGENDARY && LegendaryPokemon.Contains(species)) | |
| || (FORCE_SWAP_SPECIES_MYTHICAL && ALLOW_SPECIES_MYTHICAL && MythicalPokemon.Contains(species)) | |
| || (FORCE_SWAP_SPECIES_PARADOX && ALLOW_SPECIES_PARADOX && ParadoxPokemon.Contains(species)) | |
| || (FORCE_SWAP_SPECIES_ALL_SPECIAL_POKEMON && SpecialPokemon.Contains(species)); | |
| if (swap) | |
| { | |
| var possible = species switch | |
| { | |
| _ when SpecialPokemon.Contains(species) && FORCE_SWAP_SPECIES_ALL_SPECIAL_POKEMON => SpecialPokemon.Select(z => (DevID)z).Except(SpeciesBanlist).ToList(), | |
| _ when LegendaryPokemon.Contains(species) && FORCE_SWAP_SPECIES_LEGENDARY => LegendaryPokemon.Select(z => (DevID)z).Except(SpeciesBanlist).ToList(), | |
| _ when MythicalPokemon.Contains(species) && FORCE_SWAP_SPECIES_MYTHICAL => MythicalPokemon.Select(z => (DevID)z).Except(SpeciesBanlist).ToList(), | |
| _ when ParadoxPokemon.Contains(species) && FORCE_SWAP_SPECIES_PARADOX => ParadoxPokemon.Select(z => (DevID)z).Except(SpeciesBanlist).ToList(), | |
| _ => throw new ArgumentException(nameof(species)), | |
| }; | |
| // if our ban list does not include any eligible species, return a random species | |
| if (possible.Count == 0) | |
| return rnd; | |
| rnd = possible[GetRandom(possible.Count)]; | |
| return (DevID)GetDevID((ushort)rnd); | |
| } | |
| return rnd; | |
| } | |
| private static (DevID Species, short Form) GetSimilarStrengthPokemon(DevID dev, short form) | |
| { | |
| // prioritize force swap, don't return similar strength | |
| if (FORCE_SWAP_SPECIES_ALL_SPECIAL_POKEMON && SpecialPokemon.Contains(GetNationalDexID((ushort)dev))) return (((DevID)GetRandomSpecies((DevID)0150), (short)GetRandomForm((DevID)0150))); | |
| if (FORCE_SWAP_SPECIES_LEGENDARY && LegendaryPokemon.Contains(GetNationalDexID((ushort)dev))) return (((DevID)GetRandomSpecies((DevID)0150), (short)GetRandomForm((DevID)0150))); | |
| if (FORCE_SWAP_SPECIES_MYTHICAL && MythicalPokemon.Contains(GetNationalDexID((ushort)dev))) return (((DevID)GetRandomSpecies((DevID)0151), (short)GetRandomForm((DevID)0151))); | |
| if (FORCE_SWAP_SPECIES_PARADOX && ParadoxPokemon.Contains(GetNationalDexID((ushort)dev))) return (((DevID)GetRandomSpecies((DevID)0978), (short)GetRandomForm((DevID)0978))); | |
| var bst = GetBaseStatTotal((ushort)dev, form); | |
| List<(DevID Species, short Form)> Possible = []; | |
| bool HasManyForms(ushort species) => species is 0025 or 0479 or 0493 or 0664 or 0665 or 0666 or 0669 or 0670 or 0671 or 0774 or 0869; | |
| foreach (var t in PersonalInfoEdited.Table.Where(z => z.IsPresentInGame && !SpeciesBanlist.Contains((DevID)z.Info.SpeciesInternal))) | |
| { | |
| var d = t.Info.SpeciesInternal; | |
| var s = t.Info.SpeciesNational; | |
| var f = t.Info.Form; | |
| // don't randomize into the same species/form | |
| if (d == (ushort)dev && f == form) | |
| continue; | |
| // inaccessible | |
| if (s is 1007 or 1008 && f != 0 || s == 1017 && f >= 4) | |
| continue; | |
| // skip battle only forms if settings disallow them | |
| if (!ALLOW_BATTLE_ONLY_FORMS && HasBattleOnlyForm(s) && f != 0) | |
| continue; | |
| // species like Vivillon and Minior dilute the pool of possible swaps, so handle their other forms separately | |
| if (HasManyForms(s) && f != 0) | |
| continue; | |
| var total = t.Base.HP + t.Base.ATK + t.Base.DEF + t.Base.SPA + t.Base.SPD + t.Base.SPE; | |
| var hi = bst + (bst / 16); | |
| var lo = bst - (bst / 16); | |
| if (total <= hi && total >= lo) | |
| { | |
| var addForm = f; | |
| if (HasManyForms(s)) | |
| addForm = GetRandomForm((DevID)d); | |
| Possible.Add(((DevID)d, (short)addForm)); | |
| } | |
| } | |
| if (Possible.Count == 0) | |
| return ((DevID)dev, (short)form); | |
| return Possible[GetRandom(Possible.Count)]; | |
| } | |
| private static (DevID Species, short Form) GetTypeThemedPokemon(byte type) | |
| { | |
| List<(DevID Species, short Form)> Possible = []; | |
| bool HasManyForms(ushort species) => species is 0025 or 0479 or 0664 or 0665 or 0666 or 0669 or 0670 or 0671 or 0774 or 0869; | |
| foreach (var t in PersonalInfoEdited.Table.Where(z => z.IsPresentInGame && !SpeciesBanlist.Contains((DevID)z.Info.SpeciesInternal) && (z.Type1 == type || z.Type2 == type))) | |
| { | |
| var d = t.Info.SpeciesInternal; | |
| var s = t.Info.SpeciesNational; | |
| var f = t.Info.Form; | |
| // inaccessible | |
| if (s is 1007 or 1008 && f != 0 || s == 1017 && f >= 4) | |
| continue; | |
| // skip battle only forms if settings disallow them | |
| if (!ALLOW_BATTLE_ONLY_FORMS && HasBattleOnlyForm(s) && f != 0) | |
| continue; | |
| // species like Vivillon and Minior dilute the pool of possible swaps, so handle their other forms separately | |
| if (HasManyForms(s) && f != 0) | |
| continue; | |
| var addForm = (short)f; | |
| if (HasManyForms(s)) | |
| addForm = GetRandomForm((DevID)d); | |
| Possible.Add(((DevID)d, (short)addForm)); | |
| } | |
| return Possible[GetRandom(Possible.Count)]; | |
| } | |
| private static PokeDataFull ForceEvolveSpeciesOnce(PokeDataFull pk) | |
| { | |
| // Combee and Salandit can only evolve if they're female | |
| if (pk.DevId is DevID.DEV_MITUHANII or DevID.DEV_YATOUMORI && pk.Sex == SexType.MALE) | |
| return pk; | |
| // female Kirlia can only evolve into Gardevoir | |
| if (pk.DevId == DevID.DEV_KIRURIA && pk.Sex == SexType.FEMALE) | |
| { | |
| pk.DevId = DevID.DEV_SAANAITO; | |
| return pk; | |
| } | |
| // male Snorunt can only evolve into Glalie | |
| if (pk.DevId == DevID.DEV_YUKIWARASI && pk.Sex == SexType.MALE) | |
| { | |
| pk.DevId = DevID.DEV_ONIGOORI; | |
| return pk; | |
| } | |
| // Toxel evolves into a different form of Toxtricity depending on its nature | |
| if (pk.DevId == DevID.DEV_EREZUN) | |
| { | |
| pk.DevId = DevID.DEV_SUTORINDAA; | |
| pk.FormId = ToxtricityNaturesAmped.Contains((byte)(pk.Seikaku - 1)) ? (short)0 : (short)1; | |
| return pk; | |
| } | |
| // does not evolve | |
| var pi = PersonalInfoVanilla.Table.FirstOrDefault(z => z.Info.SpeciesInternal == (ushort)pk.DevId && z.Info.Form == pk.FormId); | |
| if (pi.Evolutions.Count == 0) | |
| return pk; | |
| // if an evolution requires a specific gender, abide by that logic, otherwise choose a random evolution index | |
| var evo = GetEvolvesIntoGenderedForm((ushort)pk.DevId, (byte)pk.FormId) && pk.Sex != SexType.DEFAULT ? (byte)(pk.Sex - 1) : GetRandom(pi.Evolutions.Count); | |
| pk.DevId = (DevID)pi.Evolutions[evo].SpeciesInternal; | |
| pk.FormId = (short)pi.Evolutions[evo].Form; | |
| return pk; | |
| } | |
| private static (DevID Species, short Form) ForceEvolveSpeciesFully(DevID species, short form) | |
| { | |
| // for the sake of difficulty, treat all Pikachu wearing a cap as the base form | |
| if (species == DevID.DEV_PIKATYUU && form != 0) | |
| form = 0; | |
| // does not evolve | |
| var stage1 = PersonalInfoVanilla.Table.FirstOrDefault(z => z.Info.SpeciesInternal == (ushort)species && z.Info.Form == form); | |
| if (stage1.Evolutions.Count == 0) | |
| return ((DevID)species, (short)form); | |
| var select1 = GetRandom(stage1.Evolutions.Count); | |
| var evoS = stage1.Evolutions[select1].SpeciesInternal; | |
| var evoF = stage1.Evolutions[select1].Form; | |
| // two-stage family | |
| var stage2 = PersonalInfoVanilla.Table.FirstOrDefault(z => z.Info.SpeciesInternal == evoS && z.Info.Form == evoF); | |
| if (stage2.Evolutions.Count == 0) | |
| return ((DevID)evoS, (short)evoF); | |
| // three-stage family | |
| var select2 = GetRandom(stage2.Evolutions.Count); | |
| return ((DevID)stage2.Evolutions[select2].SpeciesInternal, (short)stage2.Evolutions[select2].Form); | |
| } | |
| private static byte GetRandomForm(DevID dev, bool generic = false) | |
| { | |
| var species = GetNationalDexID((ushort)dev); | |
| // never allowed in battle, but okay as props | |
| if (generic && species is 0647 or 1007 or 1008) | |
| return (byte)GetRandom(2); | |
| // allow battle-only forms according to setting | |
| if (HasBattleOnlyForm(species) && !ALLOW_BATTLE_ONLY_FORMS) | |
| return species == 0774 ? (byte)GetRandom(7) : (byte)0; | |
| // should always be in base form when encountered | |
| if (IsForceBaseForm(species)) | |
| return 0; | |
| var count = GetFormCount((ushort)dev); | |
| return species switch | |
| { | |
| 0025 or 0080 => GetRandomFormWithGap(species), // disallow Partner Pikachu, Mega Slowbro | |
| 0658 => (byte)GetRandom(2), // disallow Ash-Greninja | |
| 0670 => (byte)GetRandom(5), // disallow Eternal Flower Floette | |
| 0800 => (byte)GetRandom(3), // disallow Ultra Necrozma | |
| 1017 => (byte)GetRandom(4), // disallow Terastallized Ogerpon | |
| _ => (byte)GetRandom(count), | |
| }; | |
| } | |
| private static SexType GetRandomGender(DevID species, short form) | |
| { | |
| var gender = PersonalInfoEdited.Table.FirstOrDefault(z => z.Info.SpeciesInternal == (ushort)species && z.Info.Form == form).Gender; | |
| return gender.Group switch | |
| { | |
| SexGroup.MALE => SexType.MALE, | |
| SexGroup.FEMALE => SexType.FEMALE, | |
| SexGroup.UNKNOWN => SexType.DEFAULT, | |
| _ => GetRandom(100) < gender.Ratio ? SexType.FEMALE : SexType.MALE, | |
| }; | |
| } | |
| private static sbyte GetSceneGender(ushort species, ushort form, SexType sex) | |
| { | |
| var gender = PersonalInfoEdited.Table.FirstOrDefault(z => z.Info.SpeciesInternal == species && z.Info.Form == form).Gender; | |
| return gender.Group switch | |
| { | |
| SexGroup.MALE => 0, | |
| SexGroup.FEMALE => 1, | |
| _ when GetHasGenderDifference(species, (short)form) => (sbyte)(sex - 1), | |
| _ => 0, | |
| }; | |
| } | |
| private static ushort GetRandomAbility(ushort species = 0, ushort form = 0) | |
| { | |
| var abilities = GetPermittedAbilities(); | |
| // some abilities only work when on certain species | |
| if (species == 0658 && form == 1) abilities.Add(210); // Battle Bond | |
| if (species == 1017 && form == 4) abilities.Add(301); // Embody Aspect (Teal Mask) | |
| if (species == 1017 && form == 5) abilities.Add(303); // Embody Aspect (Wellspring Mask) | |
| if (species == 1017 && form == 6) abilities.Add(302); // Embody Aspect (Hearthflame Mask) | |
| if (species == 1017 && form == 7) abilities.Add(304); // Embody Aspect (Cornerstone Mask) | |
| if (species == 1024 && form == 1) abilities.Add(308); // Tera Shell | |
| if (species == 1024 && form == 2) abilities.Add(309); // Teraform Zero | |
| if (species == 0978) abilities.Add(279); // Commander | |
| if (species == 1025) abilities.Add(310); // Poison Puppeteer | |
| return (ushort)abilities[GetRandom(abilities.Count)]; | |
| } | |
| private static TokuseiType GetRandomAbilitySlot() | |
| { | |
| var max = ALLOW_RANDOM_HIDDEN_ABILITIES ? 5 : 4; | |
| return (TokuseiType)GetRandom(2, max); | |
| } | |
| private static SeikakuType GetRandomNature(DevID species, short form) | |
| { | |
| // Toxtricity's form is dependent on nature | |
| if (species == DevID.DEV_SUTORINDAA) | |
| { | |
| var natures = form == 0 ? ToxtricityNaturesAmped : ToxtricityNaturesLowKey; | |
| return (SeikakuType)((natures[GetRandom(natures.Count)]) + 1); | |
| } | |
| return (SeikakuType)GetRandom(1, 26); | |
| } | |
| private static (DevID Species, sbyte Form) GetBandMember(BandType type, DevID dev, sbyte form) | |
| { | |
| // not eligible for a band | |
| if (type == BandType.NONE) | |
| return (DevID.DEV_NULL, 0); | |
| var species = GetNationalDexID((ushort)dev); | |
| bool IsForceSameEntry(ushort species) => species is 0386 or 0479 or 0492 or 0647 or 0648 or 0664 or 0665 or 0666 or 0774 or 0778 or 0845 or 0854 or 0855 or 0875 or 0877 or 0888 or 0889 or 0905 or 0999 or 1012 or 1013; | |
| bool IsSpecialConsideration(ushort species, sbyte form) => species == 0977 || species is 0646 or 0800 or 0845 or 0898 && form != 0; | |
| bool IsStrongerForm(ushort species) => species is 0720 or 0964; | |
| // band members can't be assigned items, so they have to be base form | |
| if (IsFormHeldItemDependent(dev, form)) | |
| return (dev, 0); | |
| // band is all the same | |
| if (type == BandType.SAME) | |
| return (dev, form); | |
| if (IsSpecialConsideration(species, form)) | |
| { | |
| // disallow Pikachu in band with Cramorant if Gen 1 is banned | |
| if (species == 0845 && form == 2 && !ALLOW_SPECIES_GEN_1) | |
| return (dev, form); | |
| // give Dondozo a random form of Tatsugiri | |
| if (species == 0977) | |
| { | |
| var sushi = DevID.DEV_SUSIDORA; | |
| var sushiForm = (sbyte)GetRandomForm(sushi); | |
| return (sushi, sushiForm); | |
| } | |
| return species switch | |
| { | |
| 0646 when form == 1 => ((DevID)0643, 0), // White Kyurem, Reshiram | |
| 0646 when form == 2 => ((DevID)0644, 0), // Black Kyurem, Zekrom | |
| 0800 when form == 1 => ((DevID)0791, 0), // Dusk Mane Necrozma, Solgaleo | |
| 0800 when form == 2 => ((DevID)0792, 0), // Dawn Wings Necrozma, Lunala | |
| 0845 when form == 1 => ((DevID)0846, 0), // Gulping Form Cramorant, Arrokuda | |
| 0845 when form == 2 => ((DevID)0025, 0), // Gorging Form Cramorant, Pikachu | |
| 0898 when form == 1 => ((DevID)0896, 0), // Ice Rider Calyrex, Glastrier | |
| 0898 when form == 2 => ((DevID)0897, 0), // Shadow Rider Calyrex, Spectrier | |
| _ => throw new ArgumentException(nameof(species)), | |
| }; | |
| } | |
| // if the boss is Hoopa or Palafin, make its band members the weaker of the two forms | |
| if (IsStrongerForm(species) && form == 1) | |
| return (dev, 0); | |
| // Gimmighoul edge case | |
| if (species == 0999) | |
| return (dev, form); | |
| var pk = PersonalInfoVanilla.Table.FirstOrDefault(z => z.Info.SpeciesInternal == (ushort)dev && z.Info.Form == form); | |
| var outSpecies = (DevID)pk.Hatch.SpeciesInternal; | |
| var outForm = (sbyte)pk.Hatch.Form; | |
| // banned cross-gen evolutionary line | |
| if (SpeciesBanlist.Contains(outSpecies)) | |
| return (dev, form); | |
| // some evolved species can fly or swim, but their pre-evolutions can not, handle these cases here | |
| bool mismatchSwim = GetCanSwimOrBeAboveWater(species, form) && !GetCanSwimOrBeAboveWater(GetNationalDexID((ushort)outSpecies), outForm); | |
| bool mismatchFly = GetCanFlyOrLevitate(species, form) && !GetCanFlyOrLevitate(GetNationalDexID((ushort)outSpecies), outForm); | |
| if (mismatchSwim || mismatchFly) | |
| { | |
| // Bellsprout, Trapinch, Litwick -> Weepinbell, Vibrava, Lampent | |
| if (species is 0071 or 0330 or 0609) | |
| dev--; | |
| return (dev, form); | |
| } | |
| // regional form | |
| if (pk.Hatch.RegionalFlags == 1) | |
| return (outSpecies, (sbyte)pk.Hatch.EverstoneForm); | |
| // workaround to give the correct form | |
| if (IsForceSameEntry(species)) | |
| return (outSpecies, form); | |
| return (outSpecies, outForm); | |
| } | |
| private static void UpdateStarterBanlist(DevID dev, short form) | |
| { | |
| var pk = PersonalInfoEdited.Table.FirstOrDefault(z => z.Info.SpeciesInternal == (ushort)dev && z.Info.Form == form); | |
| var sharedSingle = new[] { 0386, 0479, 0483, 0484, 0487, 0492, 0493, 0641, 0642, 0645, 0646, 0647, 0648, 0720, 0741, 0774, 0800, 0801, 0876, 0888, 0889, 0893, 0898, 0905, 0931, 0978, 1017, 1024 }; | |
| var sharedLine = new[] { 0025, 0026, 0174, 0422, 0423, 0550, 0585, 0586, 0658, 0664, 0665, 0666, 0669, 0670, 0671, 0854, 0855, 0902, 0963, 0964, 1012, 1013 }; | |
| List<(DevID Species, short Form)> Result = []; | |
| var species = pk.Info.SpeciesNational; | |
| var evos = pk.Evolutions; | |
| var hatch = pk.Hatch; | |
| // evolutions | |
| if (evos.Count != 0) | |
| { | |
| foreach (var t in evos) | |
| { | |
| Result.Add(((DevID)t.SpeciesInternal, (short)t.Form)); | |
| var evo = PersonalInfoEdited.Table.FirstOrDefault(z => z.Info.SpeciesInternal == t.SpeciesInternal && z.Info.Form == t.Form); | |
| foreach (var e in evo.Evolutions) | |
| Result.Add(((DevID)e.SpeciesInternal, (short)e.Form)); | |
| } | |
| } | |
| // pre-evolutions | |
| if ((ushort)dev != hatch.SpeciesInternal && species != 0490) | |
| { | |
| var regionalForm = hatch.RegionalFlags == 1 ? hatch.EverstoneForm : hatch.Form; | |
| Result.Add(((DevID)hatch.SpeciesInternal, (short)regionalForm)); | |
| var preEvo = PersonalInfoEdited.Table.FirstOrDefault(z => z.Info.SpeciesInternal == hatch.SpeciesInternal && z.Info.Form == regionalForm); | |
| foreach (var e in preEvo.Evolutions) | |
| { | |
| Result.Add(((DevID)e.SpeciesInternal, (short)e.Form)); | |
| var evo = PersonalInfoEdited.Table.FirstOrDefault(z => z.Info.SpeciesInternal == e.SpeciesInternal && z.Info.Form == e.Form); | |
| foreach (var k in evo.Evolutions) | |
| Result.Add(((DevID)k.SpeciesInternal, (short)k.Form)); | |
| } | |
| } | |
| if (species is 0656 or 0657 or 0658) Result.Add((DevID.DEV_GEKKOUGA, 1)); // Greninja (Battle Bond) | |
| if (species is 0216 or 0217 or 0901) Result.Add((DevID.DEV_HIMEGUMA3, 1)); // Ursaluna (Bloodmoon) | |
| // Raichu, Pichu | |
| if (species is 0026 or 0172) | |
| { | |
| var caps = new short[] {1,2,3,4,5,6,7,9}; | |
| foreach (var t in caps) | |
| Result.Add((DevID.DEV_PIKATYUU, t)); | |
| } | |
| // Basculegion | |
| if (species == 0550 && form is 0 or 1) | |
| { | |
| Result.Add((DevID.DEV_rBASURAO2, 0)); | |
| Result.Add((DevID.DEV_rBASURAO2, 1)); | |
| } | |
| // Rockruff, Lycanroc | |
| if (species is 0744 or 0745) | |
| { | |
| var toAddR = form == 0 ? (short)1 : (short)0; | |
| var toAddL = form == 0 ? new short[] {2} : new short[] {0,1}; | |
| Result.Add((DevID.DEV_IWANKO, toAddR)); | |
| foreach (var t in toAddL) | |
| Result.Add((DevID.DEV_RUGARUGAN, t)); | |
| } | |
| // Gimmighoul | |
| if (species == 0999 && form == 0) Result.Add((DevID.DEV_KOIN1, 1)); | |
| if (species == 0999 && form == 1) Result.Add((DevID.DEV_KOIN1, 0)); | |
| // Gholdengo | |
| if (species == 1000) | |
| { | |
| Result.Add((DevID.DEV_KOIN1, 0)); | |
| Result.Add((DevID.DEV_KOIN1, 1)); | |
| } | |
| // ban other forms | |
| if (sharedSingle.Contains(species)) | |
| { | |
| var fc = GetFormCount((ushort)dev); | |
| for (short i = 0; i < fc; i++) | |
| { | |
| if (!GetIsPresentInGame((ushort)dev, (byte)i)) | |
| continue; | |
| Result.Add(((DevID)dev, i)); | |
| } | |
| } | |
| if (sharedLine.Contains(species)) | |
| { | |
| foreach (var t in evos) | |
| { | |
| var evo1 = PersonalInfoEdited.Table.FirstOrDefault(z => z.Info.SpeciesInternal == t.SpeciesInternal && z.Info.Form == t.Form); | |
| var fc1 = GetFormCount(t.SpeciesInternal); | |
| for (short i = 0; i < fc1; i++) | |
| { | |
| if (!GetIsPresentInGame(t.SpeciesInternal, (byte)i)) | |
| continue; | |
| Result.Add(((DevID)t.SpeciesInternal, i)); | |
| } | |
| foreach (var e in evo1.Evolutions) | |
| { | |
| var fc2 = GetFormCount(e.SpeciesInternal); | |
| for (short i = 0; i < fc2; i++) | |
| { | |
| if (!GetIsPresentInGame(e.SpeciesInternal, (byte)i)) | |
| continue; | |
| Result.Add(((DevID)e.SpeciesInternal, i)); | |
| } | |
| } | |
| } | |
| var fc = GetFormCount((ushort)dev); | |
| for (short i = 0; i < fc; i++) | |
| { | |
| if (!GetIsPresentInGame((ushort)dev, (byte)i)) | |
| continue; | |
| Result.Add(((DevID)dev, i)); | |
| } | |
| var preEvo = PersonalInfoEdited.Table.FirstOrDefault(z => z.Info.SpeciesInternal == hatch.SpeciesInternal && z.Info.Form == hatch.Form); | |
| var fcPre = GetFormCount(preEvo.Info.SpeciesInternal); | |
| for (short i = 0; i < fcPre; i++) | |
| { | |
| if (!GetIsPresentInGame(preEvo.Info.SpeciesInternal, (byte)i)) | |
| continue; | |
| Result.Add(((DevID)preEvo.Info.SpeciesInternal, i)); | |
| } | |
| foreach (var t in preEvo.Evolutions) | |
| { | |
| var fcPreAfter = GetFormCount(t.SpeciesInternal); | |
| for (short i = 0; i < fcPreAfter; i++) | |
| { | |
| if (!GetIsPresentInGame(t.SpeciesInternal, (byte)i)) | |
| continue; | |
| Result.Add(((DevID)t.SpeciesInternal, i)); | |
| } | |
| } | |
| } | |
| Result.Add((dev, form)); | |
| Result = Result.Where(z => z.Species != 0).OrderBy(z => z).Distinct().ToList(); | |
| foreach (var t in Result) | |
| CurrentlyAssignedStarters.Add((t.Species, t.Form)); | |
| } | |
| private static TokuseiType GetRandomAbilitySetter() => ALLOW_RANDOM_HIDDEN_ABILITIES ? TokuseiType.RANDOM_123 : TokuseiType.RANDOM_12; | |
| private static byte GetRandomType() => (byte)GetRandom(MAX_TYPE_ID + 1); | |
| private static BallType GetRandomBall() => (BallType)GetRandom(1, MAX_BALL_ID + 1); | |
| private static RibbonType GetRandomRibbonOrMark() => (RibbonType)GetRandom(1, MAX_RIBBON_ID + 1); | |
| private static MahoippuViewID GetRandomAlcremieTopping() => (MahoippuViewID)GetRandom(7); | |
| private static int GetModifiedLevel(int level) => Math.Clamp((int)(level * (1.0f + ((float)LEVEL_MODIFIER_PERCENT / 100.0f))), 1, 100); | |
| private static ItemID GetRandomItem() => (ItemID)ItemList[GetRandom(ItemList.Count)]; | |
| private static WazaSet GetCleanMoveSlot() => new WazaSet() { WazaId = WazaID.WAZA_NULL, PointUp = 0 }; | |
| private static RareType RollForRandomShiny() => GetRandom(4096) == 0 ? RareType.RARE : RareType.NO_RARE; | |
| private static ulong GetRandomTM() | |
| { | |
| var tm = TechnicalMachineItems; | |
| tm.Remove(1230); // unused TM00 | |
| return (ulong)tm[GetRandom(tm.Count)]; | |
| } | |
| private static bool IsFormHeldItemDependent(DevID species, short form) => GetNationalDexID((ushort)species) is 0483 or 0484 or 0487 or 0493 or 0888 or 0889 or 1017 && form != 0; | |
| private static ItemID GetFormSpecificItem(DevID dev, short form) | |
| { | |
| var species = GetNationalDexID((ushort)dev); | |
| if (species is 0493 or 1017) | |
| return GetFormSpecificItemSingleSpecies(species, form); | |
| var item = species switch | |
| { | |
| 0483 => ItemID.ITEMID_DAIKONGOUDAMA, // Adamant Crystal | |
| 0484 => ItemID.ITEMID_DAISIRATAMA, // Lustrous Globe | |
| 0487 => ItemID.ITEMID_DAIHAKKINDAMA, // Griseous Core | |
| 0888 => ItemID.ITEMID_KUTITATURUGI, // Rusted Sword | |
| 0889 => ItemID.ITEMID_KUTITATATE, // Rusted Shield | |
| _ => throw new ArgumentException(nameof(species)), | |
| }; | |
| if (form != 0) | |
| return item; | |
| if (GIVE_POKEMON_RANDOM_HELD_ITEMS) | |
| { | |
| // roll for a random item, but do not give the form-specific item | |
| var rnd = GetRandomItem(); | |
| while (rnd == item) | |
| rnd = GetRandomItem(); | |
| return rnd; | |
| } | |
| return ItemID.ITEMID_NONE; | |
| } | |
| private static ItemID GetFormSpecificItemSingleSpecies(ushort species, short form) | |
| { | |
| var items = species switch | |
| { | |
| 0493 => new[] { 0000, 0303, 0306, 0304, 0305, 0309, 0308, 0310, 0313, 0298, 0299, 0301, 0300, 0307, 0302, 0311, 0312, 0644 }, | |
| 1017 => new[] { 0000, 2407, 2408, 2406, 0000, 2407, 2408, 2406 }, | |
| _ => throw new ArgumentException(nameof(species)), | |
| }; | |
| if (GIVE_POKEMON_RANDOM_HELD_ITEMS && form == 0) | |
| { | |
| // roll for a random item, but do not give the form-specific item | |
| var rnd = GetRandomItem(); | |
| while (items.Contains((ushort)rnd)) | |
| rnd = GetRandomItem(); | |
| return rnd; | |
| } | |
| return (ItemID)items[form]; | |
| } | |
| private static List<ItemID> GetRandomItemList(bool material = false) | |
| { | |
| var items = ItemList.Select(z => (ItemID)z).ToList(); | |
| ShuffleData(items); | |
| return items; | |
| } | |
| private static List<int> GetRandomMoveList() | |
| { | |
| List<int> Banned = | |
| [ | |
| 000, // ——— | |
| 165, // Struggle | |
| 464, // Dark Void | |
| 621, // Hyperspace Fury | |
| 783, // Aura Wheel | |
| ]; | |
| if (!ALLOW_STARMOBILE_TORQUE_MOVES) | |
| Banned.AddRange(Enumerable.Range(896, 5)); | |
| var moves = PermittedMoves.Except(Banned).ToList(); | |
| ShuffleData(moves); | |
| return moves; | |
| } | |
| private static List<WazaID> GetRandomSmeargleMoveset() => GetRandomMoveList()[..14].Select(z => (WazaID)z).Where(z => GetMoveCanSketch((ushort)z)).ToList(); | |
| private static List<int> GetRandomMoveListWithBias(ushort species, byte form, byte type1, byte type2) | |
| { | |
| List<int> Moves = []; | |
| foreach (var t in MoveInfoEdited.Table.Where(z => z.MoveID is not (000 or 165) && z.CanUseMove && (z.Type == type1 || z.Type == type2))) | |
| { | |
| if (IsLimitedUseMove(t.MoveID, species, form)) | |
| continue; | |
| Moves.Add(t.MoveID); | |
| } | |
| ShuffleData(Moves); | |
| return Moves; | |
| } | |
| private static ushort[] GetImportantSpeciesMoves(ushort species) => species switch | |
| { | |
| 0057 => [889], // Rage Fist | |
| 0190 => [458], // Double Hit | |
| 0193 or 0221 => [246], // Ancient Power | |
| 0203 => [888], // Twin Beam | |
| 0206 => [887], // Hyper Drill | |
| 0211 => [839], // Barb Barrage | |
| 0234 => [828], // Psyshield Bash | |
| 0438 => [102], // Mimic | |
| 0550 => [834], // Wave Crash | |
| 0648 => [547], // Relic Song | |
| 0762 => [023], // Stomp | |
| 0845 => [057, 291], // Surf, Dive | |
| _ => throw new ArgumentException(nameof(species)), | |
| }; | |
| private static bool IsLimitedUseMove(ushort move, ushort species, byte form) => move switch | |
| { | |
| 464 when species != 0491 => true, // Darkrai: Dark Void | |
| 783 when species != 0877 => true, // Morpeko: Aura Wheel | |
| 621 when species != 0720 && form != 1 => true, // Hoopa Unbound: Hyperspace Fury | |
| >= 896 and <= 900 when !ALLOW_STARMOBILE_TORQUE_MOVES => true, // Starmobile: Torque moves | |
| _ => false, | |
| }; | |
| private static byte GetRandomFormWithGap(ushort species) | |
| { | |
| var count = GetFormCount(species); | |
| var banned = species == 0025 ? 8 : 1; | |
| var form = (byte)GetRandom(count - 1); | |
| if (form == banned) | |
| form++; | |
| return form; | |
| } | |
| private static GemType GetTeraType(DevID species, short form, bool specific = false) | |
| { | |
| // Ogerpon: must match form | |
| if (species == DevID.DEV_KAMENONI) | |
| { | |
| return form switch | |
| { | |
| 0 or 4 => GemType.KUSA, // Teal Mask | |
| 1 or 5 => GemType.MIZU, // Wellspring Mask | |
| 2 or 6 => GemType.HONOO, // Hearthflame Mask | |
| 3 or 7 => GemType.IWA, // Cornerstone Mask | |
| _ => throw new ArgumentException(nameof(form)), | |
| }; | |
| } | |
| // Terapagos: only Stellar | |
| if (species == DevID.DEV_KODAIGAME) | |
| return GemType.NIJI; | |
| // disallow random, abide by standard types | |
| if (!ALLOW_FULLY_RANDOM_TERA_TYPES) | |
| return specific ? GetStandardTeraType((ushort)species, form) : GemType.DEFAULT; | |
| // roll for a chance of Stellar, otherwise random | |
| if (GetRandom(100) < STELLAR_TERA_TYPE_CHANCE_PERCENT) | |
| return GemType.NIJI; | |
| return specific ? (GemType)GetRandom(2, 20) : GemType.RANDOM; | |
| } | |
| private static GemType GetStandardTeraType(ushort species, short form) | |
| { | |
| var pi = PersonalInfoEdited.Table.FirstOrDefault(z => z.Info.SpeciesInternal == species && z.Info.Form == form); | |
| var gem = GetRandom(2) == 0 ? (GemType)(pi.Type1 + 2) : (GemType)(pi.Type2 + 2); | |
| return gem; | |
| } | |
| private static bool IsFormValidForWildEncounter(ushort dev, byte form) | |
| { | |
| var species = GetNationalDexID(dev); | |
| // allow battle-only forms according to setting | |
| if (HasBattleOnlyForm(species) && !ALLOW_BATTLE_ONLY_FORMS) | |
| return species == 0774 ? form <= 6 : form == 0; | |
| // Keldeo, Koraidon, and Miraidon should only appear in base form | |
| if (species is 0647 or 1007 or 1008) | |
| return form == 0; | |
| // validate Ogerpon type for potential Tera Type mismatch | |
| if (species == 1017) | |
| { | |
| var types = GetPokemonTypes((DevID)dev, form); | |
| if (form == 0 && types.Type1 == (byte)TypeIndex.Grass && types.Type2 == (byte)TypeIndex.Grass) return true; | |
| if (form == 1 && types.Type1 == (byte)TypeIndex.Water && types.Type2 == (byte)TypeIndex.Water) return true; | |
| if (form == 2 && types.Type1 == (byte)TypeIndex.Fire && types.Type2 == (byte)TypeIndex.Fire) return true; | |
| if (form == 3 && types.Type1 == (byte)TypeIndex.Rock && types.Type2 == (byte)TypeIndex.Rock) return true; | |
| return false; | |
| } | |
| return true; | |
| } | |
| private static PersonalTable GetVanillaPersonalTable() | |
| { | |
| var file = Path.Combine(PATH_MASTER, "avalon/data/personal_array.bin"); | |
| var obj = FlatBufferConverter.DeserializeFrom<PersonalTable>(file); | |
| return obj; | |
| } | |
| private static void UpdatePersonalTable() | |
| { | |
| var dest = Path.Combine(DEST_MASTER, "avalon/data/"); | |
| var data = FlatBufferConverter.SerializeFrom(PersonalInfoEdited); | |
| if (!Directory.Exists(dest)) | |
| Directory.CreateDirectory(dest); | |
| File.WriteAllBytes(Path.Combine(dest, "personal_array.bin"), data); | |
| } | |
| private static WazaTable GetVanillaMoveTable() | |
| { | |
| var file = Path.Combine(PATH_MASTER, "avalon/data/waza_array.bin"); | |
| var obj = FlatBufferConverter.DeserializeFrom<WazaTable>(file); | |
| return obj; | |
| } | |
| private static void UpdateMoveTable() | |
| { | |
| var dest = Path.Combine(DEST_MASTER, "avalon/data/"); | |
| var data = FlatBufferConverter.SerializeFrom(MoveInfoEdited); | |
| if (!Directory.Exists(dest)) | |
| Directory.CreateDirectory(dest); | |
| File.WriteAllBytes(Path.Combine(dest, "waza_array.bin"), data); | |
| } | |
| private static ushort GetDevID(ushort species) => PersonalInfoVanilla.Table.FirstOrDefault(z => z.Info.SpeciesNational == species).Info.SpeciesInternal; | |
| private static ushort GetNationalDexID(ushort species) => PersonalInfoVanilla.Table.FirstOrDefault(z => z.Info.SpeciesInternal == species).Info.SpeciesNational; | |
| private static bool GetIsPresentInGame(ushort species, byte form) => PersonalInfoVanilla.Table.FirstOrDefault(z => z.Info.SpeciesInternal == species && z.Info.Form == form).IsPresentInGame; | |
| private static byte GetFormCount(ushort species) => (byte)PersonalInfoVanilla.Table.Where(z => z.Info.SpeciesInternal == species).ToArray().Length; | |
| private static SexGroup GetPokemonGenderGroup(ushort species, sbyte form) => PersonalInfoEdited.Table.FirstOrDefault(z => z.Info.SpeciesInternal == species && z.Info.Form == form).Gender.Group; | |
| private static byte GetEvoStage(ushort species, byte form) => PersonalInfoVanilla.Table.FirstOrDefault(z => z.Info.SpeciesInternal == species && z.Info.Form == form).EvoStage; | |
| private static byte GetBaseFriendship(ushort species, byte form) => PersonalInfoVanilla.Table.FirstOrDefault(z => z.Info.SpeciesInternal == species && z.Info.Form == form).BaseFriendship; | |
| private static bool GetMoveCanFlinch(ushort move) => MoveInfoEdited.Table.FirstOrDefault(z => z.MoveID == move).Flinch is >= 1 and <= 99; | |
| private static bool GetMoveCanInflictBurn(ushort move) => MoveInfoEdited.Table.FirstOrDefault(z => z.MoveID == move).Inflict.Value == 4; | |
| private static bool GetMoveCanInflictConfusion(ushort move) => MoveInfoEdited.Table.FirstOrDefault(z => z.MoveID == move).Inflict.Value == 6; | |
| private static bool GetMoveCanInflictPoison(ushort move) => MoveInfoEdited.Table.FirstOrDefault(z => z.MoveID == move).Inflict.Value is 5 or 47; | |
| private static bool GetMoveCanInflictSleep(ushort move) => MoveInfoEdited.Table.FirstOrDefault(z => z.MoveID == move).Inflict.Value is 2 or 47; | |
| private static bool GetMoveCanSketch(ushort move) => !MoveInfoEdited.Table.FirstOrDefault(z => z.MoveID == move).FlagNoSketch; | |
| private static ushort GetMoveEffectSequence(ushort move) => MoveInfoEdited.Table.FirstOrDefault(z => z.MoveID == move).EffectSequence; | |
| private static bool GetMoveIsBinding(ushort move) => MoveInfoEdited.Table.FirstOrDefault(z => z.MoveID == move).Inflict.Value == 8; | |
| private static bool GetMoveIsContact(ushort move) => MoveInfoEdited.Table.FirstOrDefault(z => z.MoveID == move).FlagMakesContact; | |
| private static bool GetMoveIsPunch(ushort move) => MoveInfoEdited.Table.FirstOrDefault(z => z.MoveID == move).FlagPunch; | |
| private static bool GetMoveIsSound(ushort move) => MoveInfoEdited.Table.FirstOrDefault(z => z.MoveID == move).FlagSound; | |
| private static bool GetMoveIsStatus(ushort move) => MoveInfoEdited.Table.FirstOrDefault(z => z.MoveID == move).Category == 0; | |
| private static bool GetMoveIsTwoTurn(ushort move) => MoveInfoEdited.Table.FirstOrDefault(z => z.MoveID == move).FlagCharge; | |
| private static byte GetMovePower(ushort move) => MoveInfoEdited.Table.FirstOrDefault(z => z.MoveID == move).Power; | |
| private static byte GetMoveType(ushort move) => MoveInfoEdited.Table.FirstOrDefault(z => z.MoveID == move).Type; | |
| private static bool GetMoveIsPhysical(ushort move, byte[] baseStats, ParamSet IVs, ParamSet EVs, int level, SeikakuType nature, ushort ability) | |
| { | |
| bool physical = MoveInfoEdited.Table.FirstOrDefault(z => z.MoveID == move).Category == 1; | |
| if (!IsMoveVariedCategory(move)) | |
| return physical; | |
| // calculate stats | |
| var statATK = CalculateStatOther(baseStats[1], StatIndex.ATK, IVs.ATK, EVs.ATK, level, nature, ability, false); | |
| var statSPA = CalculateStatOther(baseStats[3], StatIndex.SPA, IVs.SPA, EVs.SPA, level, nature, ability, false); | |
| if (statATK == statSPA) | |
| return physical; | |
| return statATK > statSPA; | |
| } | |
| private static bool GetMoveIsSpecial(ushort move, byte[] baseStats, ParamSet IVs, ParamSet EVs, int level, SeikakuType nature, ushort ability) | |
| { | |
| bool special = MoveInfoEdited.Table.FirstOrDefault(z => z.MoveID == move).Category == 2; | |
| if (!IsMoveVariedCategory(move)) | |
| return special; | |
| // calculate stats | |
| var statATK = CalculateStatOther(baseStats[1], StatIndex.ATK, IVs.ATK, EVs.ATK, level, nature, ability, false); | |
| var statSPA = CalculateStatOther(baseStats[3], StatIndex.SPA, IVs.SPA, EVs.SPA, level, nature, ability, false); | |
| if (statATK == statSPA) | |
| return special; | |
| return statATK < statSPA; | |
| } | |
| private static byte GetMoveAccuracy(ushort move, byte[] baseStats, ParamSet IVs, ParamSet EVs, int level, SeikakuType nature, ushort ability, bool poison) | |
| { | |
| // Toxic, if the user is Poison-type | |
| if (move == 092 && poison) | |
| return 101; | |
| var accuracy = MoveInfoEdited.Table.FirstOrDefault(z => z.MoveID == move).Accuracy; | |
| // never misses | |
| if (accuracy == 101) | |
| return accuracy; | |
| // Compound Eyes: 30% increase | |
| if (ability == 014) | |
| return (byte)Math.Clamp((int)(accuracy * (5325.0f / 4096.0f)), 1, 100); | |
| // Hustle: 20% decrease | |
| if (ability == 055 && GetMoveIsPhysical(move, baseStats, IVs, EVs, level, nature, ability)) | |
| return (byte)Math.Clamp((int)(accuracy * (3277.0f / 4096.0f)), 1, 100); | |
| // No Guard | |
| if (ability == 099) | |
| return 101; | |
| // Victory Star: 10% increase | |
| // this can technically stack in Double Battles, but always assume only 1 is active | |
| if (ability == 162) | |
| return (byte)Math.Clamp((int)(accuracy * (4506.0f / 4096.0f)), 1, 100); | |
| return accuracy; | |
| } | |
| private static ushort CalculateStatHP(byte statBase, int iv, int ev, int level) => (ushort)(statBase == 1 ? 1 : ((iv + (2 * statBase) + (ev / 4) + 100) * level / 100) + 10); | |
| private static ushort CalculateStatOther(byte statBase, StatIndex index, int iv, int ev, int level, SeikakuType nature, ushort ability, bool consider) | |
| { | |
| bool boost15 = ability is 055 or 255 && consider; | |
| bool boost20 = ability is 037 or 074 && consider; | |
| var stat = (ushort)(((iv + (2 * statBase) + (ev / 4)) * level / 100) + 5); | |
| // Nature modifier | |
| stat = (ushort)(PKHeX.Core.NatureAmp.AmplifyStat((PKHeX.Core.Nature)(nature - 1), (int)index, stat)); | |
| // Hustle, Gorilla Tactics | |
| if (boost15 && index == StatIndex.ATK) | |
| stat = (ushort)(stat * 1.50f); | |
| // Huge Power, Pure Power | |
| if (boost20 && index == StatIndex.ATK) | |
| stat *= 2; | |
| return stat; | |
| } | |
| private static byte CalculateChampionsMovePP(byte pp) | |
| { | |
| if (pp == 1) | |
| return 1; | |
| return (byte)Math.Clamp((4 * (pp / 5 + 1)), 8, 20); | |
| } | |
| private static bool |