Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Rework Monster Groups (2/2) #52323

Merged
merged 4 commits into from
Oct 23, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
42 changes: 29 additions & 13 deletions doc/JSON_INFO.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ Use the `Home` key to return to the top.
- [Fuel data](#fuel-data)
- [Monster Groups](#monster-groups)
- [Group definition](#group-definition)
- [Monster definition](#monster-definition)
- [Monster/Subgroup definition](#monstersubgroup-definition)
- [Monster Factions](#monster-factions)
- [Monsters](#monsters)
- [Mutation Categories](#mutation-categories)
Expand Down Expand Up @@ -938,30 +938,46 @@ If a fuel has the PERPETUAL flag, engines powered by it never use any fuel. Thi
| `new_monster_group_id` | (_optional_) (string) The id of the monster group that should replace this one.
| `replacement_time` | (_optional_) (int) The amount of time before the group should be replaced by the new one, in days. Final replacement date is calculated by `replacement_time * evolution factor`.

#### Monster definition
#### Monster/Subgroup definition

In monster groups, within the `"monsters"` array, you can define `"group"` objects as well as `"monster"` objects. Groups use the same fields as monsters, but they are processed differently. When the game looks for possible spawns from a monster group, it will recursively check subgroups if they exist. The weight of the subgroup is defined just like monster objects, so spawn chances only matter for top-level objects.

| Identifier | Description
|--- |---
| `monster` | The monster's unique ID, eg. `"mon_zombie"`.
| `monster` | The monster's unique ID, eg. `"mon_zombie"`. Indicates that this entry is a "monster".
| `group` | The sub-group's unique ID eg. `"GROUP_ZOMBIE"`. Indicates that this entry is a "monstergroup".
| `weight` | (_optional_) Chance of occurrence (`weight` / total `weight` in group) (default: 1)
| `cost_multiplier` | (_optional_) How many monsters each monster in this definition should count as, if spawning a limited number of monsters. (default: 1)
| `pack_size` | (_optional_) The minimum and maximum number of monsters in this group that should spawn together. (default: `[1,1]`)
| `conditions` | (_optional_) Conditions limit when monsters spawn. Valid options: `SUMMER`, `WINTER`, `AUTUMN`, `SPRING`, `DAY`, `NIGHT`, `DUSK`, `DAWN`. Multiple Time-of-day conditions (`DAY`, `NIGHT`, `DUSK`, `DAWN`) will be combined together so that any of those conditions makes the spawn valid. Multiple Season conditions (`SUMMER`, `WINTER`, `AUTUMN`, `SPRING`) will be combined together so that any of those conditions makes the spawn valid.
| `starts` | (_optional_) This entry becomes active after this time. Specified using time units. (**multiplied by the evolution scaling factor**)
| `ends` | (_optional_) This entry becomes inactive after this time. Specified using time units. (**multiplied by the evolution scaling factor**)
| `spawn_data` | (_optional_) Any properties that the monster only has when spawned in this group. `ammo` defines how much of which ammo types the monster spawns with.
| `spawn_data` | (_optional_) Any properties that the monster only has when spawned in this group. `ammo` defines how much of which ammo types the monster spawns with. Only applies to "monster" type entries.

```C++
// Example of a monstergroup containing only "monster" entries:
{
"name" : "GROUP_ANT",
"default" : "mon_ant",
"monsters" : [
{ "monster" : "mon_ant", "weight" : 870, "cost_multiplier" : 0 },
{ "monster" : "mon_ant_larva", "weight" : 40, "cost_multiplier" : 0 },
{ "monster" : "mon_ant_soldier", "weight" : 90, "cost_multiplier" : 5 },
{ "monster" : "mon_ant_queen", "weight" : 0, "cost_multiplier" : 0 },
{ "monster" : "mon_thing", "weight" : 100, "cost_multiplier" : 0, "pack_size" : [3,5], "conditions" : ["DUSK","DAWN","SUMMER"] }
]
"name" : "GROUP_ANT",
"default" : "mon_ant",
"monsters" : [
{ "monster" : "mon_ant", "weight" : 870, "cost_multiplier" : 0 },
{ "monster" : "mon_ant_larva", "weight" : 40, "cost_multiplier" : 0 },
{ "monster" : "mon_ant_soldier", "weight" : 90, "cost_multiplier" : 5 },
{ "monster" : "mon_ant_queen", "weight" : 0, "cost_multiplier" : 0 },
{ "monster" : "mon_thing", "weight" : 100, "cost_multiplier" : 0, "pack_size" : [3,5], "conditions" : ["DUSK","DAWN","SUMMER"] }
]
},
// Example of a monstergroup containing subgroups:
{
"type": "monstergroup",
"name": "GROUP_MIGO_RAID",
"//": "Meta-group for mi-gos on-the-go.",
"monsters": [
{ "group": "GROUP_MI-GO_BASE_CAPTORS", "weight": 150, "cost_multiplier": 6, "pack_size": [ 1, 2 ] },
{ "group": "GROUP_MI-GO_SCOUT_TOWER", "weight": 100, "cost_multiplier": 4, "pack_size": [ 0, 2 ] },
{ "monster": "mon_mi_go_guard", "weight": 200, "cost_multiplier": 4 },
{ "monster": "mon_mi_go", "weight": 500, "cost_multiplier": 2, "pack_size": [ 3, 4 ] }
]
}
```

Expand Down
2 changes: 1 addition & 1 deletion src/activity_handlers.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -2842,7 +2842,7 @@ static void rod_fish( Character *you, const std::vector<monster *> &fishables )
//if the vector is empty (no fish around) the player is still given a small chance to get a (let us say it was hidden) fish
if( fishables.empty() ) {
const std::vector<mtype_id> fish_group = MonsterGroupManager::GetMonstersFromGroup(
mongroup_id( "GROUP_FISH" ) );
mongroup_id( "GROUP_FISH" ), true );
const mtype_id fish_mon = random_entry_ref( fish_group );
here.add_item_or_charges( you->pos(), item::make_corpse( fish_mon, calendar::turn + rng( 0_turns,
3_hours ) ) );
Expand Down
2 changes: 1 addition & 1 deletion src/gamemode_defense.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1353,7 +1353,7 @@ std::vector<mtype_id> defense_game::pick_monster_wave()
if( valid.empty() ) {
debugmsg( "Couldn't find a valid monster group for defense!" );
} else {
ret = MonsterGroupManager::GetMonstersFromGroup( random_entry( valid ) );
ret = MonsterGroupManager::GetMonstersFromGroup( random_entry( valid ), true );
}

return ret;
Expand Down
2 changes: 1 addition & 1 deletion src/iuse.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1935,7 +1935,7 @@ cata::optional<int> iuse::fish_trap( Character *p, item *it, bool t, const tripo
//lets say it is a 5% chance per fish to catch
if( one_in( 20 ) ) {
const std::vector<mtype_id> fish_group = MonsterGroupManager::GetMonstersFromGroup(
GROUP_FISH );
GROUP_FISH, true );
const mtype_id &fish_mon = random_entry_ref( fish_group );
//Yes, we can put fishes in the trap like knives in the boot,
//and then get fishes via activation of the item,
Expand Down
2 changes: 1 addition & 1 deletion src/map_extras.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -2760,7 +2760,7 @@ static bool mx_grave( map &m, const tripoint &abs_sub )
m.put_items_from_loc( item_group_id( "everyday_corpse" ), corpse_location );
} else {
//Pets' corpses
const std::vector<mtype_id> pets = MonsterGroupManager::GetMonstersFromGroup( GROUP_PETS );
const std::vector<mtype_id> pets = MonsterGroupManager::GetMonstersFromGroup( GROUP_PETS, true );
const mtype_id &pet = random_entry_ref( pets );
item body = item::make_corpse( pet, calendar::start_of_cataclysm );
m.add_item_or_charges( corpse_location, body );
Expand Down
90 changes: 76 additions & 14 deletions src/mongroup.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,12 @@ float mongroup::avg_speed() const
const MonsterGroup &g = type.obj();
int remaining_frequency = g.freq_total;
for( const MonsterGroupEntry &elem : g.monsters ) {
avg_speed += elem.frequency * elem.name.obj().speed;
if( elem.is_group() ) {
// TODO: recursively derive average speed from subgroups
avg_speed += elem.frequency * 100;
} else {
avg_speed += elem.frequency * elem.name.obj().speed;
}
remaining_frequency -= elem.frequency;
}
if( remaining_frequency > 0 ) {
Expand Down Expand Up @@ -101,7 +106,7 @@ 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 )
const mongroup_id &group_name, int *quantity, bool *mon_found )
{
const MonsterGroup &group = GetUpgradedMonsterGroup( group_name );
int spawn_chance = rng( 1, group.freq_total ); //Default 1000 unless specified
Expand All @@ -117,6 +122,19 @@ MonsterGroupResult MonsterGroupManager::GetResultFromGroup(
for( auto it = group.monsters.begin(); it != group.monsters.end() && !monster_found; ++it ) {
// There's a lot of conditions to work through to see if this spawn definition is valid
bool valid_entry = true;
// Check for monsters within subgroup
if( it->is_group() ) {
MonsterGroupResult tmp = GetResultFromGroup( it->group, quantity, &monster_found );
if( monster_found ) {
// Valid monster found withing 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() ||
Expand Down Expand Up @@ -200,6 +218,9 @@ MonsterGroupResult MonsterGroupManager::GetResultFromGroup(
if( quantity && !monster_found ) {
( *quantity )--;
}
if( mon_found ) {
( *mon_found ) = monster_found;
}

return spawn_details;
}
Expand Down Expand Up @@ -229,14 +250,23 @@ const mongroup_id &MonsterGroupManager::Monster2Group( const mtype_id &monster )
return mongroup_id::NULL_ID();
}

std::vector<mtype_id> MonsterGroupManager::GetMonstersFromGroup( const mongroup_id &group )
std::vector<mtype_id> MonsterGroupManager::GetMonstersFromGroup( const mongroup_id &group,
bool from_subgroups )
{
const MonsterGroup &g = group.obj();

std::vector<mtype_id> monsters;

for( const MonsterGroupEntry &elem : g.monsters ) {
monsters.push_back( elem.name );
if( elem.is_group() ) {
if( from_subgroups ) {
std::vector<mtype_id> submons = GetMonstersFromGroup( elem.group, from_subgroups );
monsters.reserve( monsters.size() + submons.size() );
monsters.insert( monsters.end(), submons.begin(), submons.end() );
}
} else {
monsters.push_back( elem.name );
}
}
return monsters;
}
Expand Down Expand Up @@ -330,7 +360,7 @@ void MonsterGroupManager::FinalizeMonsterGroups()
for( auto &elem : monsterGroupMap ) {
MonsterGroup &mg = elem.second;
for( FreqDef::iterator c = mg.monsters.begin(); c != mg.monsters.end(); ) {
if( MonsterGroupManager::monster_is_blacklisted( c->name ) ) {
if( !c->is_group() && MonsterGroupManager::monster_is_blacklisted( c->name ) ) {
c = mg.monsters.erase( c );
} else {
++c;
Expand Down Expand Up @@ -362,14 +392,23 @@ void MonsterGroupManager::LoadMonsterGroup( const JsonObject &jo )
g.is_animal = jo.get_bool( "is_animal", false );
if( jo.has_array( "monsters" ) ) {
for( JsonObject mon : jo.get_array( "monsters" ) ) {
const mtype_id name = mtype_id( mon.get_string( "monster" ) );
bool isgroup = false;
std::string id_name;
if( mon.has_string( "group" ) ) {
isgroup = true;
id_name = mon.get_string( "group" );
} else {
id_name = mon.get_string( "monster" );
}

int freq = mon.get_int( "weight", 1 );
if( mon.has_int( "freq" ) ) {
freq = mon.get_int( "freq" );
}
if( freq > max_freq.second ) {
max_freq = { name, freq };
if( !isgroup ) {
max_freq = { mtype_id( id_name ), freq };
}
}
freq_total += freq;
int cost = mon.get_int( "cost_multiplier", 1 );
Expand Down Expand Up @@ -401,8 +440,11 @@ void MonsterGroupManager::LoadMonsterGroup( const JsonObject &jo )
}
}
}
MonsterGroupEntry new_mon_group = MonsterGroupEntry( name, freq, cost, pack_min, pack_max, data,
starts, ends );
MonsterGroupEntry new_mon_group = isgroup ?
MonsterGroupEntry( mongroup_id( id_name ), freq, cost,
pack_min, pack_max, data, starts, ends ) :
MonsterGroupEntry( mtype_id( id_name ), freq, cost, pack_min,
pack_max, data, starts, ends );
if( mon.has_member( "conditions" ) ) {
for( const std::string line : mon.get_array( "conditions" ) ) {
new_mon_group.conditions.push_back( line );
Expand Down Expand Up @@ -450,16 +492,33 @@ void MonsterGroupManager::ClearMonsterGroups()
monster_categories_whitelist.clear();
}

static void check_group_def( const mongroup_id &g )
{
for( const auto &m : g.obj().monsters ) {
if( m.is_group() ) {
if( !m.group.is_valid() ) {
debugmsg( "monster group %s contains unknown subgroup %s", g.c_str(), m.group.c_str() );
} else {
check_group_def( m.group );
}
} else if( !m.name.is_valid() ) {
debugmsg( "monster group %s contains unknown monster %s", g.c_str(), m.name.c_str() );
}
}
}

void MonsterGroupManager::check_group_definitions()
{
for( auto &e : monsterGroupMap ) {
const MonsterGroup &mg = e.second;
if( !mg.defaultMonster.is_valid() ) {
debugmsg( "monster group %s has unknown default monster %s", mg.name.c_str(),
mg.defaultMonster.c_str() );
}
for( const auto &mge : mg.monsters ) {
if( !mge.name.is_valid() ) {
if( mge.is_group() ) {
if( !mge.group.is_valid() ) {
debugmsg( "monster group %s contains unknown subgroup %s", mg.name.c_str(), mge.group.c_str() );
} else {
check_group_def( mge.group );
}
} else if( !mge.name.is_valid() ) {
// mon_null should not be valid here
debugmsg( "monster group %s contains unknown monster %s", mg.name.c_str(), mge.name.c_str() );
}
Expand All @@ -473,6 +532,9 @@ const mtype_id &MonsterGroupManager::GetRandomMonsterFromGroup( const mongroup_i
int spawn_chance = rng( 1, group.freq_total );
for( const auto &monster_type : group.monsters ) {
if( monster_type.frequency >= spawn_chance ) {
if( monster_type.is_group() ) {
return GetRandomMonsterFromGroup( monster_type.group );
}
return monster_type.name;
} else {
spawn_chance -= monster_type.frequency;
Expand Down
26 changes: 24 additions & 2 deletions src/mongroup.h
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ using FreqDef_iter = FreqDef::iterator;

struct MonsterGroupEntry {
mtype_id name;
mongroup_id group;
int frequency;
int cost_multiplier;
int pack_minimum;
Expand All @@ -37,11 +38,29 @@ struct MonsterGroupEntry {
bool lasts_forever() const {
return ends <= 0_turns;
}
bool is_group() const {
return group != mongroup_id();
}

MonsterGroupEntry( const mtype_id &id, int new_freq, int new_cost, int new_pack_min,
int new_pack_max, const spawn_data &new_data, const time_duration &new_starts,
const time_duration &new_ends )
: name( id )
, group( mongroup_id() )
, frequency( new_freq )
, cost_multiplier( new_cost )
, pack_minimum( new_pack_min )
, pack_maximum( new_pack_max )
, data( new_data )
, starts( new_starts )
, ends( new_ends ) {
}

MonsterGroupEntry( const mongroup_id &id, int new_freq, int new_cost, int new_pack_min,
int new_pack_max, const spawn_data &new_data, const time_duration &new_starts,
const time_duration &new_ends )
: name( mtype_id() )
, group( id )
, frequency( new_freq )
, cost_multiplier( new_cost )
, pack_minimum( new_pack_min )
Expand Down Expand Up @@ -174,11 +193,12 @@ 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 );
static MonsterGroupResult GetResultFromGroup( const mongroup_id &group, int *quantity = nullptr,
bool *mon_found = nullptr );
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 );
static std::vector<mtype_id> GetMonstersFromGroup( const mongroup_id &group );
static std::vector<mtype_id> GetMonstersFromGroup( const mongroup_id &group, bool from_subgroups );
static const MonsterGroup &GetMonsterGroup( const mongroup_id &group );
static const MonsterGroup &GetUpgradedMonsterGroup( const mongroup_id &group );
/**
Expand All @@ -195,6 +215,8 @@ class MonsterGroupManager

static bool is_animal( const mongroup_id &group );

static void extract_mons_from_subgroups();

private:
static std::map<mongroup_id, MonsterGroup> monsterGroupMap;
using t_string_set = std::set<std::string>;
Expand Down