From 2f227d4490d13b1ddd3aa046261b63e96e6d943f Mon Sep 17 00:00:00 2001 From: David Seguin Date: Tue, 19 Jul 2022 13:50:59 -0400 Subject: [PATCH] Mongroup: subgroup spawning fixes (#59281) * Mongroup: do not expand subgroup when spawning * Mongroup: unit test * Mongroup: use parent pack_size to select # of subentries + unit test --- data/mods/TEST_DATA/monstergroups.json | 41 ++++++++++ src/explosion.cpp | 13 ++-- src/map.cpp | 31 ++++---- src/map_field.cpp | 24 +++--- src/mapgen.cpp | 40 ++++++---- src/mongroup.cpp | 56 ++++++++------ src/mongroup.h | 4 +- src/player_hardcoded_effects.cpp | 16 ++-- tests/mongroup_test.cpp | 103 ++++++++++++++++++++++++- 9 files changed, 247 insertions(+), 81 deletions(-) diff --git a/data/mods/TEST_DATA/monstergroups.json b/data/mods/TEST_DATA/monstergroups.json index 2939296502a4f..8689b69a82534 100644 --- a/data/mods/TEST_DATA/monstergroups.json +++ b/data/mods/TEST_DATA/monstergroups.json @@ -56,5 +56,46 @@ { "group": "test_event_mongroup", "weight": 10 }, { "group": "test_event_only", "weight": 5 } ] + }, + { + "name": "test_l2_nested_mongroup", + "type": "monstergroup", + "monsters": [ + { "monster": "mon_test_speed_desc_base", "weight": 50 }, + { "monster": "mon_test_speed_desc_base_immobile", "weight": 50 } + ] + }, + { + "name": "test_l1_nested_mongroup", + "type": "monstergroup", + "monsters": [ + { "group": "test_l2_nested_mongroup", "weight": 5 }, + { "monster": "mon_test_shearable", "weight": 50 }, + { "monster": "mon_test_non_shearable", "weight": 50 } + ] + }, + { + "name": "test_top_level_mongroup", + "type": "monstergroup", + "monsters": [ + { "group": "test_l1_nested_mongroup", "weight": 5 }, + { "monster": "mon_test_CBM", "weight": 5 }, + { "monster": "mon_test_bovine", "weight": 5 } + ] + }, + { + "name": "test_nested_packsize", + "type": "monstergroup", + "monsters": [ { "monster": "mon_test_CBM", "pack_size": [ 2, 4 ] } ] + }, + { + "name": "test_top_level_packsize", + "type": "monstergroup", + "monsters": [ { "group": "test_nested_packsize", "pack_size": [ 4, 6 ] } ] + }, + { + "name": "test_top_level_no_packsize", + "type": "monstergroup", + "monsters": [ { "group": "test_nested_packsize" } ] } ] diff --git a/src/explosion.cpp b/src/explosion.cpp index cb3ddd52514a9..c5646ba388ed8 100644 --- a/src/explosion.cpp +++ b/src/explosion.cpp @@ -783,7 +783,6 @@ void resonance_cascade( const tripoint &p ) Character &player_character = get_player_character(); const time_duration maxglow = time_duration::from_turns( 100 - 5 * trig_dist( p, player_character.pos() ) ); - MonsterGroupResult spawn_details; if( maxglow > 0_turns ) { const time_duration minglow = std::max( 0_turns, time_duration::from_turns( 60 - 5 * trig_dist( p, player_character.pos() ) ) ); @@ -846,10 +845,14 @@ void resonance_cascade( const tripoint &p ) break; case 13: case 14: - case 15: - spawn_details = MonsterGroupManager::GetResultFromGroup( GROUP_NETHER ); - g->place_critter_at( spawn_details.name, dest ); - break; + case 15: { + std::vector spawn_details = + MonsterGroupManager::GetResultFromGroup( GROUP_NETHER ); + for( const MonsterGroupResult &mgr : spawn_details ) { + g->place_critter_at( mgr.name, dest ); + } + } + break; case 16: case 17: case 18: diff --git a/src/map.cpp b/src/map.cpp index acdbd6abece26..9e9d2cf07a94d 100644 --- a/src/map.cpp +++ b/src/map.cpp @@ -7518,8 +7518,10 @@ void map::rotten_item_spawn( const item &item, const tripoint &pnt ) } if( rng( 0, 100 ) < comest->rot_spawn_chance ) { - MonsterGroupResult spawn_details = MonsterGroupManager::GetResultFromGroup( mgroup ); - add_spawn( spawn_details, pnt ); + std::vector spawn_details = MonsterGroupManager::GetResultFromGroup( mgroup ); + for( const MonsterGroupResult &mgr : spawn_details ) { + add_spawn( mgr, pnt ); + } if( get_player_view().sees( pnt ) ) { if( item.is_seed() ) { add_msg( m_warning, _( "Something has crawled out of the %s plants!" ), item.get_plant_name() ); @@ -8019,18 +8021,21 @@ void map::spawn_monsters_submap_group( const tripoint &gp, mongroup &group, if( pop ) { // Populate the group from its population variable. for( int m = 0; m < pop; m++ ) { - MonsterGroupResult spawn_details = MonsterGroupManager::GetResultFromGroup( group.type, &pop ); - if( !spawn_details.name ) { - continue; - } - monster tmp( spawn_details.name ); + std::vector spawn_details = + MonsterGroupManager::GetResultFromGroup( group.type, &pop ); + for( const MonsterGroupResult &mgr : spawn_details ) { + if( !mgr.name ) { + continue; + } + monster tmp( mgr.name ); - // If a monster came from a horde population, configure them to always be willing to rejoin a horde. - if( group.horde ) { - tmp.set_horde_attraction( MHA_ALWAYS ); - } - for( int i = 0; i < spawn_details.pack_size; i++ ) { - group.monsters.push_back( tmp ); + // If a monster came from a horde population, configure them to always be willing to rejoin a horde. + if( group.horde ) { + tmp.set_horde_attraction( MHA_ALWAYS ); + } + for( int i = 0; i < mgr.pack_size; i++ ) { + group.monsters.push_back( tmp ); + } } } } diff --git a/src/map_field.cpp b/src/map_field.cpp index 5df4ad9e3ca12..8a07e7db43d6a 100644 --- a/src/map_field.cpp +++ b/src/map_field.cpp @@ -742,17 +742,19 @@ static void field_processor_monster_spawn( const tripoint &p, field_entry &cur, int monster_spawn_count = int_level.monster_spawn_count; if( monster_spawn_count > 0 && monster_spawn_chance > 0 && one_in( monster_spawn_chance ) ) { for( ; monster_spawn_count > 0; monster_spawn_count-- ) { - MonsterGroupResult spawn_details = MonsterGroupManager::GetResultFromGroup( - int_level.monster_spawn_group, &monster_spawn_count ); - if( !spawn_details.name ) { - continue; - } - if( const cata::optional spawn_point = random_point( - points_in_radius( p, int_level.monster_spawn_radius ), - [&pd]( const tripoint & n ) { - return pd.here.passable( n ); - } ) ) { - pd.here.add_spawn( spawn_details, *spawn_point ); + std::vector spawn_details = + MonsterGroupManager::GetResultFromGroup( int_level.monster_spawn_group, &monster_spawn_count ); + for( const MonsterGroupResult &mgr : spawn_details ) { + if( !mgr.name ) { + continue; + } + if( const cata::optional spawn_point = + random_point( points_in_radius( p, int_level.monster_spawn_radius ), + [&pd]( const tripoint & n ) { + return pd.here.passable( n ); + } ) ) { + pd.here.add_spawn( mgr, *spawn_point ); + } } } } diff --git a/src/mapgen.cpp b/src/mapgen.cpp index 2a67e891ee8bb..05b983cd98930 100644 --- a/src/mapgen.cpp +++ b/src/mapgen.cpp @@ -267,15 +267,18 @@ void map::generate( const tripoint &p, const time_point &when ) if( spawns.group && x_in_y( odds_after_density, 100 ) ) { int pop = spawn_count * rng( spawns.population.min, spawns.population.max ); for( ; pop > 0; pop-- ) { - MonsterGroupResult spawn_details = MonsterGroupManager::GetResultFromGroup( spawns.group, &pop ); - if( !spawn_details.name ) { - continue; - } - if( const cata::optional pt = - random_point( *this, [this]( const tripoint & n ) { - return passable( n ); - } ) ) { - add_spawn( spawn_details, *pt ); + std::vector spawn_details = + MonsterGroupManager::GetResultFromGroup( spawns.group, &pop ); + for( const MonsterGroupResult &mgr : spawn_details ) { + if( !mgr.name ) { + continue; + } + if( const cata::optional pt = + random_point( *this, [this]( const tripoint & n ) { + return passable( n ); + } ) ) { + add_spawn( mgr, *pt ); + } } } } @@ -2355,11 +2358,13 @@ class jmapgen_monster : public jmapgen_piece mongroup_id chosen_group = m_id.get( dat ); if( !chosen_group.is_null() ) { - MonsterGroupResult spawn_details = + std::vector spawn_details = MonsterGroupManager::GetResultFromGroup( chosen_group ); - dat.m.add_spawn( spawn_details.name, spawn_count * pack_size.get(), - { x.get(), y.get(), dat.m.get_abs_sub().z() }, - friendly, -1, mission_id, name, data ); + for( const MonsterGroupResult &mgr : spawn_details ) { + dat.m.add_spawn( mgr.name, spawn_count * pack_size.get(), + { x.get(), y.get(), dat.m.get_abs_sub().z() }, + friendly, -1, mission_id, name, data ); + } } else { mtype_id chosen_type = ids.pick()->get( dat ); if( !chosen_type.is_null() ) { @@ -6433,9 +6438,12 @@ void map::place_spawns( const mongroup_id &group, const int chance, } while( impassable( p ) && tries > 0 ); // Pick a monster type - MonsterGroupResult spawn_details = MonsterGroupManager::GetResultFromGroup( group, &num ); - add_spawn( spawn_details.name, spawn_details.pack_size, { p, abs_sub.z() }, - friendly, -1, mission_id, name, spawn_details.data ); + std::vector spawn_details = + MonsterGroupManager::GetResultFromGroup( group, &num ); + for( const MonsterGroupResult &mgr : spawn_details ) { + add_spawn( mgr.name, mgr.pack_size, { p, abs_sub.z() }, + friendly, -1, mission_id, name, mgr.data ); + } } } diff --git a/src/mongroup.cpp b/src/mongroup.cpp index 4baf16f6ee7f6..575b3035df065 100644 --- a/src/mongroup.cpp +++ b/src/mongroup.cpp @@ -106,13 +106,13 @@ const MonsterGroup &MonsterGroupManager::GetUpgradedMonsterGroup( const mongroup } //Quantity is adjusted directly as a side effect of this function -MonsterGroupResult MonsterGroupManager::GetResultFromGroup( - const mongroup_id &group_name, int *quantity, bool *mon_found ) +std::vector MonsterGroupManager::GetResultFromGroup( + const mongroup_id &group_name, int *quantity, bool *mon_found, bool from_subgroup ) { const MonsterGroup &group = GetUpgradedMonsterGroup( group_name ); int spawn_chance = rng( 1, group.event_adjusted_freq_total() ); //Our spawn details specify, by default, a single instance of the default monster - MonsterGroupResult spawn_details = MonsterGroupResult( group.defaultMonster, 1, spawn_data() ); + std::vector spawn_details; bool monster_found = false; // Loop invariant values @@ -131,19 +131,6 @@ MonsterGroupResult MonsterGroupManager::GetResultFromGroup( valid_entry = false; } - // Check for monsters within subgroup - if( valid_entry && it->is_group() ) { - MonsterGroupResult tmp = GetResultFromGroup( it->group, quantity, &monster_found ); - if( monster_found ) { - // Valid monster found within subgroup, break early - spawn_details = tmp; - break; - } else if( quantity ) { - // Nothing found in subgroup, reset quantity - ( *quantity )++; - } - continue; - } //Insure that the time is not before the spawn first appears or after it stops appearing valid_entry = valid_entry && ( calendar::start_of_cataclysm + it->starts < calendar::turn ); valid_entry = valid_entry && ( it->lasts_forever() || @@ -201,19 +188,34 @@ MonsterGroupResult MonsterGroupManager::GetResultFromGroup( valid_entry = false; } + const int pack_size = it->pack_maximum > 1 ? rng( it->pack_minimum, it->pack_maximum ) : 1; + + // Check for monsters within subgroup + bool found_in_subgroup = false; + int tmp_qty = !!quantity ? *quantity : 1; + std::vector tmp_grp_list; + if( valid_entry && it->is_group() ) { + for( int i = 0; i < pack_size; i++ ) { + std::vector tmp_grp = + GetResultFromGroup( it->group, !!quantity ? &tmp_qty : nullptr, &found_in_subgroup, true ); + tmp_grp_list.insert( tmp_grp_list.end(), tmp_grp.begin(), tmp_grp.end() ); + } + } + //If the entry was valid, check to see if we actually spawn it if( valid_entry ) { //If the monsters frequency is greater than the spawn_chance, select this spawn rule if( it->frequency >= spawn_chance ) { - if( it->pack_maximum > 1 ) { - spawn_details = MonsterGroupResult( it->name, rng( it->pack_minimum, it->pack_maximum ), it->data ); + if( found_in_subgroup ) { + //If spawned from a subgroup, we've already obtained that data + spawn_details = tmp_grp_list; } else { - spawn_details = MonsterGroupResult( it->name, 1, it->data ); - } - //And if a quantity pointer with remaining value was passed, will modify the external value as a side effect - //We will reduce it by the spawn rule's cost multiplier - if( quantity ) { - *quantity -= std::max( 1, it->cost_multiplier * spawn_details.pack_size ); + spawn_details.emplace_back( MonsterGroupResult( it->name, pack_size, it->data ) ); + //And if a quantity pointer with remaining value was passed, will modify the external value as a side effect + //We will reduce it by the spawn rule's cost multiplier + if( quantity ) { + *quantity -= std::max( 1, it->cost_multiplier * pack_size ); + } } monster_found = true; //Otherwise, subtract the frequency from spawn result for the next loop around @@ -224,13 +226,17 @@ MonsterGroupResult MonsterGroupManager::GetResultFromGroup( } // Force quantity to decrement regardless of whether we found a monster. - if( quantity && !monster_found ) { + if( quantity && !monster_found && !from_subgroup ) { ( *quantity )--; } if( mon_found ) { ( *mon_found ) = monster_found; } + if( !from_subgroup && spawn_details.empty() ) { + spawn_details.emplace_back( MonsterGroupResult( group.defaultMonster, 1, spawn_data() ) ); + } + return spawn_details; } diff --git a/src/mongroup.h b/src/mongroup.h index 17fd405b8dd41..c2befd6b872f8 100644 --- a/src/mongroup.h +++ b/src/mongroup.h @@ -201,8 +201,8 @@ class MonsterGroupManager static void LoadMonsterBlacklist( const JsonObject &jo ); static void LoadMonsterWhitelist( const JsonObject &jo ); static void FinalizeMonsterGroups(); - static MonsterGroupResult GetResultFromGroup( const mongroup_id &group, int *quantity = nullptr, - bool *mon_found = nullptr ); + static std::vector GetResultFromGroup( const mongroup_id &group, + int *quantity = nullptr, bool *mon_found = nullptr, bool from_subgroup = false ); static bool IsMonsterInGroup( const mongroup_id &group, const mtype_id &monster ); static bool isValidMonsterGroup( const mongroup_id &group ); static const mongroup_id &Monster2Group( const mtype_id &monster ); diff --git a/src/player_hardcoded_effects.cpp b/src/player_hardcoded_effects.cpp index 7ed7ddaf4d6a2..7178b66e75894 100644 --- a/src/player_hardcoded_effects.cpp +++ b/src/player_hardcoded_effects.cpp @@ -592,9 +592,11 @@ static void eff_fun_teleglow( Character &u, effect &it ) if( here.impassable( dest ) ) { here.make_rubble( dest, f_rubble_rock, true ); } - MonsterGroupResult spawn_details = MonsterGroupManager::GetResultFromGroup( - GROUP_NETHER ); - g->place_critter_at( spawn_details.name, dest ); + std::vector spawn_details = + MonsterGroupManager::GetResultFromGroup( GROUP_NETHER ); + for( const MonsterGroupResult &mgr : spawn_details ) { + g->place_critter_at( mgr.name, dest ); + } if( uistate.distraction_hostile_spotted && player_character.sees( dest ) ) { g->cancel_activity_or_ignore_query( distraction_type::hostile_spotted_far, _( "A monster appears nearby!" ) ); @@ -1300,9 +1302,11 @@ void Character::hardcoded_effects( effect &it ) if( here.impassable( dest ) ) { here.make_rubble( dest, f_rubble_rock, true ); } - MonsterGroupResult spawn_details = MonsterGroupManager::GetResultFromGroup( - GROUP_NETHER ); - g->place_critter_at( spawn_details.name, dest ); + std::vector spawn_details = + MonsterGroupManager::GetResultFromGroup( GROUP_NETHER ); + for( const MonsterGroupResult &mgr : spawn_details ) { + g->place_critter_at( mgr.name, dest ); + } if( uistate.distraction_hostile_spotted && player_character.sees( dest ) ) { g->cancel_activity_or_ignore_query( distraction_type::hostile_spotted_far, _( "A monster appears nearby!" ) ); diff --git a/tests/mongroup_test.cpp b/tests/mongroup_test.cpp index 395a2a0594090..975d69c984da6 100644 --- a/tests/mongroup_test.cpp +++ b/tests/mongroup_test.cpp @@ -25,12 +25,12 @@ static void spawn_x_monsters( int x, const mongroup_id &grp, const std::vector rand_results; calendar::turn = time_point( 1 ); for( int i = 0; i < x; i++ ) { - const mtype_id &tmp_get = MonsterGroupManager::GetRandomMonsterFromGroup( grp ); + mtype_id tmp_get = MonsterGroupManager::GetRandomMonsterFromGroup( grp ); if( !tmp_get.is_null() ) { rand_gets.emplace( tmp_get ); } - const mtype_id &tmp_res = MonsterGroupManager::GetResultFromGroup( grp ).name; + mtype_id tmp_res = MonsterGroupManager::GetResultFromGroup( grp ).front().name; if( !tmp_res.is_null() ) { rand_results.emplace( tmp_res ); } @@ -151,4 +151,101 @@ TEST_CASE( "Using mon_null as mongroup default monster", "[mongroup]" ) CHECK( test_group2->defaultMonster == mon_null ); CHECK( test_group3->defaultMonster == mon_null ); CHECK( test_group4->defaultMonster != mon_null ); -} \ No newline at end of file +} + +TEST_CASE( "Nested monster groups spawn chance", "[mongroup]" ) +{ + mongroup_id mg( "test_top_level_mongroup" ); + + const int iters = 10000; + + // < result, < layer, expected prob, count > > + std::map> results { + // top layer - 33.3% + { mon_test_CBM, { 0, 1.f / 3.f, 0 } }, + { mon_test_bovine, { 0, 1.f / 3.f, 0 } }, + // nested layer 1 - 15.9% (47.6%) + { mon_test_shearable, { 1, ( 1.f / 3.f ) *( 50.f / 105.f ), 0 } }, + { mon_test_non_shearable, { 1, ( 1.f / 3.f ) *( 50.f / 105.f ), 0 } }, + // nested layer 2 - 0.8% (2.4% (50%)) + { mon_test_speed_desc_base, { 2, ( ( 1.f / 3.f ) * ( 5.f / 105.f ) ) * 0.5f, 0 } }, + { mon_test_speed_desc_base_immobile, { 2, ( ( 1.f / 3.f ) * ( 5.f / 105.f ) ) * 0.5f, 0 } } + }; + + // < layer, < expected prob, count > > + std::map> layers { + // top layer - 66.7% + { 0, { 2.f / 3.f, 0 } }, + // nested layer 1 - 31.7% (95.2%) + { 1, { ( 1.f / 3.f ) *( 100.f / 105.f ), 0 } }, + // nested layer 2 - 1.6% (4.8% (100%)) + { 2, { ( 1.f / 3.f ) *( 5.f / 105.f ), 0 } } + }; + + calendar::turn += 1_turns; + + for( int i = 0; i < iters; i++ ) { + MonsterGroupResult res = MonsterGroupManager::GetResultFromGroup( mg ).front(); + auto iter = results.find( res.name ); + CAPTURE( res.name.c_str() ); + REQUIRE( iter != results.end() ); + if( iter != results.end() ) { + layers[std::get<0>( iter->second )].second++; + std::get<2>( results[res.name] )++; + } + } + + for( const auto &lyr : layers ) { + INFO( string_format( "layer %d - expected vs. actual", lyr.first ) ); + CHECK( lyr.second.first == + Approx( static_cast( lyr.second.second ) / iters ).epsilon( 0.5 ) ); + } + + for( const auto &res : results ) { + INFO( string_format( "monster %s - expected vs. actual", res.first.c_str() ) ); + CHECK( std::get<1>( res.second ) == + Approx( static_cast( std::get<2>( res.second ) ) / iters ).epsilon( 0.5 ) ); + } +} + +TEST_CASE( "Nested monster group pack size", "[mongroup]" ) +{ + const int iters = 100; + calendar::turn += 1_turns; + + SECTION( "Nested group pack size used as-is" ) { + mongroup_id mg( "test_top_level_no_packsize" ); + for( int i = 0; i < iters; i++ ) { + bool found = false; + std::vector res = + MonsterGroupManager::GetResultFromGroup( mg, nullptr, &found ); + REQUIRE( found ); + + // pack_size == [2, 4] * 1 + CHECK( res.size() == 1 ); + int total = 0; + for( const MonsterGroupResult &mgr : res ) { + total += mgr.pack_size; + } + CHECK( total == Approx( 3 ).margin( 1 ) ); + } + } + + SECTION( "Nested group pack size multiplied by top level pack size" ) { + mongroup_id mg( "test_top_level_packsize" ); + for( int i = 0; i < iters; i++ ) { + bool found = false; + std::vector res = + MonsterGroupManager::GetResultFromGroup( mg, nullptr, &found ); + REQUIRE( found ); + + // pack_size == [2, 4] * [4, 6] + CHECK( res.size() == Approx( 5 ).margin( 1 ) ); + int total = 0; + for( const MonsterGroupResult &mgr : res ) { + total += mgr.pack_size; + } + CHECK( total == Approx( 16 ).margin( 8 ) ); + } + } +}