Skip to content

Instantly share code, notes, and snippets.

@sora10pls
Last active June 29, 2026 02:51
Show Gist options
  • Select an option

  • Save sora10pls/07d69600241f3a47959b802560ab8900 to your computer and use it in GitHub Desktop.

Select an option

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.
// 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