diff --git a/src/EnemyType.cc b/src/EnemyType.cc index 8bacadd0..dc565d59 100644 --- a/src/EnemyType.cc +++ b/src/EnemyType.cc @@ -158,6 +158,8 @@ const char* phosg::name_for_enum(EnemyType type) { return "LOVE_RAPPY"; case EnemyType::MERICAROL: return "MERICAROL"; + case EnemyType::MERICARAND: + return "MERICARAND"; case EnemyType::MERICUS: return "MERICUS"; case EnemyType::MERIKLE: @@ -348,6 +350,7 @@ EnemyType phosg::enum_for_name(const char* name) { {"KONDRIEU", EnemyType::KONDRIEU}, {"LA_DIMENIAN", EnemyType::LA_DIMENIAN}, {"LOVE_RAPPY", EnemyType::LOVE_RAPPY}, + {"MERICARAND", EnemyType::MERICARAND}, {"MERICAROL", EnemyType::MERICAROL}, {"MERICUS", EnemyType::MERICUS}, {"MERIKLE", EnemyType::MERIKLE}, @@ -500,6 +503,7 @@ bool enemy_type_valid_for_episode(Episode episode, EnemyType enemy_type) { case EnemyType::LA_DIMENIAN: case EnemyType::LOVE_RAPPY: case EnemyType::MERICAROL: + case EnemyType::MERICARAND: case EnemyType::MERICUS: case EnemyType::MERIKLE: case EnemyType::MERILLIA: @@ -744,6 +748,7 @@ uint8_t battle_param_index_for_enemy_type(Episode episode, EnemyType enemy_type) case EnemyType::MIGIUM: return 0x33; case EnemyType::MERICAROL: + case EnemyType::MERICARAND: return 0x3A; case EnemyType::UL_GIBBON: return 0x3B; @@ -979,6 +984,7 @@ uint8_t rare_table_index_for_enemy_type(EnemyType enemy_type) { case EnemyType::LOVE_RAPPY: return 0x33; case EnemyType::MERICAROL: + case EnemyType::MERICARAND: return 0x38; case EnemyType::MERICUS: return 0x3A; @@ -1116,6 +1122,8 @@ bool enemy_type_is_rare(EnemyType type) { (type == EnemyType::AL_RAPPY) || (type == EnemyType::NAR_LILY) || (type == EnemyType::POUILLY_SLIME) || + (type == EnemyType::MERICUS) || + (type == EnemyType::MERIKLE) || (type == EnemyType::MERISSA_AA) || (type == EnemyType::PAZUZU_ALT) || (type == EnemyType::DORPHON_ECLAIR) || diff --git a/src/EnemyType.hh b/src/EnemyType.hh index 0f5fcc7d..7e705794 100644 --- a/src/EnemyType.hh +++ b/src/EnemyType.hh @@ -80,6 +80,7 @@ enum class EnemyType { KONDRIEU, LA_DIMENIAN, LOVE_RAPPY, + MERICARAND, MERICAROL, MERICUS, MERIKLE, diff --git a/src/Map.cc b/src/Map.cc index e6877edc..c61608a4 100644 --- a/src/Map.cc +++ b/src/Map.cc @@ -2066,12 +2066,9 @@ shared_ptr SuperMap::add_enemy_and_children( break; } case 0x0064: { // TObjEneSlime - // TODO: It appears BB doesn't have a way to force slimes to be rare via - // constructor args. Is this true? + // Unlike all other versions, BB doesn't have a way to force slimes to be + // rare via constructor args bool is_rare_v123 = (set_entry->uparam2 & 1); - if ((set_entry->num_children != 0) && (set_entry->num_children != 4)) { - this->log.warning("POFUILLY_SLIME has an unusual num_children (0x%hX)", set_entry->num_children.load()); - } default_num_children = -1; // Skip adding children later (because we do it here) size_t num_children = set_entry->num_children ? set_entry->num_children.load() : 4; for (size_t z = 0; z < num_children + 1; z++) { @@ -2224,15 +2221,24 @@ shared_ptr SuperMap::add_enemy_and_children( default_num_children = 4; break; case 0x00D5: // TObjEneMerillLia - add((set_entry->uparam1 & 0x01) ? EnemyType::MERILTAS : EnemyType::MERILLIA); + add((set_entry->uparam1 & 1) ? EnemyType::MERILTAS : EnemyType::MERILLIA); break; - case 0x00D6: // TObjEneBm9Mericarol - if (set_entry->uparam1 == 0) { - add(EnemyType::MERICAROL); - } else { - add(((set_entry->uparam1 % 3) == 2) ? EnemyType::MERICUS : EnemyType::MERIKLE); + case 0x00D6: { // TObjEneBm9Mericarol + switch (set_entry->uparam1) { + case 0: + add(EnemyType::MERICAROL); + break; + case 1: + add(EnemyType::MERIKLE); + break; + case 2: + add(EnemyType::MERICUS); + break; + default: + add(EnemyType::MERICARAND); } break; + } case 0x00D7: // TObjEneBm5GibonU add((set_entry->uparam1 & 0x01) ? EnemyType::ZOL_GIBBON : EnemyType::UL_GIBBON); break; @@ -3218,29 +3224,31 @@ void SuperMap::print(FILE* stream) const { //////////////////////////////////////////////////////////////////////////////// // Map state -MapState::RareEnemyRates::RareEnemyRates(uint32_t enemy_rate, uint32_t boss_rate) +MapState::RareEnemyRates::RareEnemyRates(uint32_t enemy_rate, uint32_t mericarand_rate, uint32_t boss_rate) : hildeblue(enemy_rate), rappy(enemy_rate), nar_lily(enemy_rate), pouilly_slime(enemy_rate), + mericarand(mericarand_rate), merissa_aa(enemy_rate), pazuzu(enemy_rate), dorphon_eclair(enemy_rate), kondrieu(boss_rate) {} MapState::RareEnemyRates::RareEnemyRates(const phosg::JSON& json) - : hildeblue(json.get_int("Hildeblue")), - rappy(json.get_int("Rappy")), - nar_lily(json.get_int("NarLily")), - pouilly_slime(json.get_int("PouillySlime")), - merissa_aa(json.get_int("MerissaAA")), - pazuzu(json.get_int("Pazuzu")), - dorphon_eclair(json.get_int("DorphonEclair")), - kondrieu(json.get_int("Kondrieu")) {} + : hildeblue(json.get_int("Hildeblue", DEFAULT_RARE_ENEMY_RATE_V3)), + rappy(json.get_int("Rappy", DEFAULT_RARE_ENEMY_RATE_V3)), + nar_lily(json.get_int("NarLily", DEFAULT_RARE_ENEMY_RATE_V3)), + pouilly_slime(json.get_int("PouillySlime", DEFAULT_RARE_ENEMY_RATE_V3)), + mericarand(json.get_int("Mericarand", DEFAULT_MERICARAND_RATE_V3)), + merissa_aa(json.get_int("MerissaAA", DEFAULT_RARE_ENEMY_RATE_V3)), + pazuzu(json.get_int("Pazuzu", DEFAULT_RARE_ENEMY_RATE_V3)), + dorphon_eclair(json.get_int("DorphonEclair", DEFAULT_RARE_ENEMY_RATE_V3)), + kondrieu(json.get_int("Kondrieu", DEFAULT_RARE_BOSS_RATE_V4)) {} string MapState::RareEnemyRates::str() const { - return phosg::string_printf("RareEnemyRates(hildeblue=%08" PRIX32 ", rappy=%08" PRIX32 ", nar_lily=%08" PRIX32 ", pouilly_slime=%08" PRIX32 ", merissa_aa=%08" PRIX32 ", pazuzu=%08" PRIX32 ", dorphon_eclair=%08" PRIX32 ", kondrieu=%08" PRIX32 ")", - this->hildeblue, this->rappy, this->nar_lily, this->pouilly_slime, + return phosg::string_printf("RareEnemyRates(hildeblue=%08" PRIX32 ", rappy=%08" PRIX32 ", nar_lily=%08" PRIX32 ", pouilly_slime=%08" PRIX32 ", mericarand=%08" PRIX32 ", merissa_aa=%08" PRIX32 ", pazuzu=%08" PRIX32 ", dorphon_eclair=%08" PRIX32 ", kondrieu=%08" PRIX32 ")", + this->hildeblue, this->rappy, this->nar_lily, this->pouilly_slime, this->mericarand, this->merissa_aa, this->pazuzu, this->dorphon_eclair, this->kondrieu); } @@ -3250,6 +3258,7 @@ phosg::JSON MapState::RareEnemyRates::json() const { {"Rappy", this->rappy}, {"NarLily", this->nar_lily}, {"PouillySlime", this->pouilly_slime}, + {"Mericarand", this->mericarand}, {"MerissaAA", this->merissa_aa}, {"Pazuzu", this->pazuzu}, {"DorphonEclair", this->dorphon_eclair}, @@ -3269,6 +3278,8 @@ uint32_t MapState::RareEnemyRates::for_enemy_type(EnemyType type) const { return this->nar_lily; case EnemyType::POFUILLY_SLIME: return this->pouilly_slime; + case EnemyType::MERICARAND: + return this->mericarand; case EnemyType::MERISSA_A: return this->merissa_aa; case EnemyType::ZU: @@ -3284,8 +3295,12 @@ uint32_t MapState::RareEnemyRates::for_enemy_type(EnemyType type) const { } } -const shared_ptr MapState::NO_RARE_ENEMIES = make_shared(0, 0); -const shared_ptr MapState::DEFAULT_RARE_ENEMIES = make_shared(0x0083126E, 0x1999999A); +const shared_ptr MapState::NO_RARE_ENEMIES = make_shared( + 0, 0, 0); +const shared_ptr MapState::DEFAULT_RARE_ENEMIES = make_shared( + MapState::RareEnemyRates::DEFAULT_RARE_ENEMY_RATE_V3, + MapState::RareEnemyRates::DEFAULT_MERICARAND_RATE_V3, + MapState::RareEnemyRates::DEFAULT_RARE_BOSS_RATE_V4); uint32_t MapState::EnemyState::convert_game_flags(uint32_t game_flags, bool to_v3) { // The format of game_flags was changed significantly between v2 and v3, and @@ -3522,7 +3537,7 @@ void MapState::index_super_map(const FloorConfig& fc, shared_ptrget_episode(), this->event, ene->floor); - if (rare_type != type) { + if ((type == EnemyType::MERICARAND) || (rare_type != type)) { unordered_map det_cache; uint32_t bb_rare_rate = this->bb_rare_rates->for_enemy_type(type); for (Version v : ALL_ARPG_SEMANTIC_VERSIONS) { @@ -3556,15 +3571,35 @@ void MapState::index_super_map(const FloorConfig& fc, shared_ptrset_rare(v); + if (type == EnemyType::MERICARAND) { + // On v3, Mericarols that have uparam1 > 2 are randomized to be + // Mericus, Merikle, or Mericarol, but the former two are not + // considered rare. (We use rare_flags anyway to distinguish them + // from Mericarol.) + if (det > 0.9) { // Merikle + ene_st->set_rare(v); + ene_st->set_mericarand_variant_flag(v); + } else if (det > 0.8) { // Mericus + ene_st->set_rare(v); + } else { + // Mericarol (no flags to set) + } + + } else { + // On v1 and v2 (and GC NTE), the rare rate is 0.1% instead of 0.2%. + if (det < (is_v1_or_v2(v) ? 0.001f : 0.002f)) { + ene_st->set_rare(v); + } } + } else if ((bb_rare_rate > 0) && (this->bb_rare_enemy_indexes.size() < 0x10) && (random_from_optional_crypt(opt_rand_crypt) < bb_rare_rate)) { this->bb_rare_enemy_indexes.emplace_back(enemy_index); ene_st->set_rare(v); + if ((type == EnemyType::MERICARAND) && (enemy_index & 1)) { + ene_st->set_mericarand_variant_flag(v); + } } } } diff --git a/src/Map.hh b/src/Map.hh index 637e3086..ba3fd49b 100644 --- a/src/Map.hh +++ b/src/Map.hh @@ -628,16 +628,22 @@ protected: class MapState { public: struct RareEnemyRates { + static constexpr uint32_t DEFAULT_RARE_ENEMY_RATE_V1_V2 = 0x00418937; // 1/1000 + static constexpr uint32_t DEFAULT_RARE_ENEMY_RATE_V3 = 0x0083126E; // 1/500 + static constexpr uint32_t DEFAULT_MERICARAND_RATE_V3 = 0x33333333; // 1/5 + static constexpr uint32_t DEFAULT_RARE_BOSS_RATE_V4 = 0x1999999A; // 1/10 + uint32_t hildeblue; // HILDEBEAR -> HILDEBLUE uint32_t rappy; // RAG_RAPPY -> {AL_RAPPY or seasonal rappies}; SAND_RAPPY -> DEL_RAPPY uint32_t nar_lily; // POISON_LILY -> NAR_LILY uint32_t pouilly_slime; // POFUILLY_SLIME -> POUILLY_SLIME + uint32_t mericarand; // MERICARAND -> MERIKLE or MERICUS (only for those with subtype > 2) uint32_t merissa_aa; // MERISSA_A -> MERISSA_AA uint32_t pazuzu; // ZU -> PAZUZU (and _ALT variants) uint32_t dorphon_eclair; // DORPHON -> DORPHON_ECLAIR uint32_t kondrieu; // {SAINT_MILLION, SHAMBERTIN} -> KONDRIEU - RareEnemyRates(uint32_t enemy_rate, uint32_t boss_rate); + RareEnemyRates(uint32_t enemy_rate, uint32_t mericarand_rate, uint32_t boss_rate); explicit RareEnemyRates(const phosg::JSON& json); uint32_t for_enemy_type(EnemyType type) const; @@ -687,9 +693,10 @@ public: }; size_t e_id = 0; size_t set_id = 0; + uint32_t game_flags = 0; // From 6x0A uint16_t total_damage = 0; uint16_t rare_flags = 0; - uint32_t game_flags = 0; // From 6x0A + uint16_t mericarand_variant_flags = 0; uint16_t set_flags = 0; // Only used if super_ene->child_index == 0 uint16_t server_flags = 0; @@ -721,16 +728,29 @@ public: } inline bool is_rare(Version version) const { - return (((rare_flags >> static_cast(version)) & 1) || + return (((this->rare_flags >> static_cast(version)) & 1) || ((version == Version::BB_V4) ? this->super_ene->is_default_rare_bb : this->super_ene->is_default_rare_v123)); } inline void set_rare(Version version) { this->rare_flags |= (1 << static_cast(version)); } + inline void set_mericarand_variant_flag(Version version) { + this->mericarand_variant_flags |= (1 << static_cast(version)); + } inline EnemyType type(Version version, Episode episode, uint8_t event) const { - return this->is_rare(version) - ? rare_type_for_enemy_type(this->super_ene->type, episode, event, this->super_ene->floor) - : this->super_ene->type; + if (this->super_ene->type == EnemyType::MERICARAND) { + if (this->is_rare(version)) { + return ((this->mericarand_variant_flags >> static_cast(version)) & 1) + ? EnemyType::MERIKLE + : EnemyType::MERICUS; + } else { + return EnemyType::MERICAROL; + } + } else { + return this->is_rare(version) + ? rare_type_for_enemy_type(this->super_ene->type, episode, event, this->super_ene->floor) + : this->super_ene->type; + } } inline bool ever_hit_by_client_id(uint8_t client_id) const { return this->server_flags & (Flag::ALL_HITS_MASK_FIRST << client_id); diff --git a/system/config.example.json b/system/config.example.json index be6f6c9b..1de125c6 100644 --- a/system/config.example.json +++ b/system/config.example.json @@ -1205,18 +1205,24 @@ // all difficulties use the same rates.) If the Challenge set is not // specified, the default rates are used for Challenge mode, which is 1/500 // for all enemies. + // The Mericarand rate applies to Mericarol enemies whose arguments specify + // that they may be replaced with Mericus or Merikle. (Specifically, if + // uparam1 > 2, this is the case.) When a Mericarol is replaced, the enemy + // index is used to determine whether it will be a Mericus (even enemy index) + // or Merikle (odd enemy index). "RareEnemyRates-Normal": { // These are probabilities out of 0xFFFFFFFF - so 0 means that rare enemy // will never appear, and 0xFFFFFFFF means it will always appear (until 16 // rare enemies have been assigned). - "Hildeblue": 0x0083126E, - "Rappy": 0x0083126E, - "NarLily": 0x0083126E, - "PouillySlime": 0x0083126E, - "MerissaAA": 0x0083126E, - "Pazuzu": 0x0083126E, - "DorphonEclair": 0x0083126E, - "Kondrieu": 0x1999999A, + "Hildeblue": 0x0083126E, // 1/500 + "Rappy": 0x0083126E, // 1/500 + "NarLily": 0x0083126E, // 1/500 + "PouillySlime": 0x0083126E, // 1/500 + "Mericarand": 0x33333333, // 1/5 + "MerissaAA": 0x0083126E, // 1/500 + "Pazuzu": 0x0083126E, // 1/500 + "DorphonEclair": 0x0083126E, // 1/500 + "Kondrieu": 0x1999999A, // 1/10 }, // "RareEnemyRates-Hard": {...}, // "RareEnemyRates-VeryHard": {...},