Skip to content

Commit

Permalink
feat: Add exclusions and exclusion categories to gunmods (#4254)
Browse files Browse the repository at this point in the history
* Add exclusion option to gunmods

Allows gunmods to exclude specific weapons/weapon categories.

* Add example of use to game.

* Documentation

* style(autofix.ci): automated formatting

* Update item.cpp

Per request for readability.

---------

Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
  • Loading branch information
KheirFerrum and autofix-ci[bot] committed Mar 2, 2024
1 parent 8c42631 commit 85f265a
Show file tree
Hide file tree
Showing 6 changed files with 86 additions and 5 deletions.
4 changes: 3 additions & 1 deletion data/json/items/gunmod/rail.json
Original file line number Diff line number Diff line change
Expand Up @@ -158,6 +158,7 @@
"color": "blue",
"location": "rail",
"mod_target_category": [ [ "SUBMACHINE_GUNS" ], [ "RIFLES" ], [ "MACHINE_GUNS" ], [ "GATLING_GUNS" ], [ "M_XBOWS" ] ],
"mod_exclusion_category": [ [ "ENERGY_WEAPONS" ] ],
"dispersion_modifier": -1,
"handling_modifier": 10
},
Expand All @@ -177,6 +178,7 @@
[ "GRENADE_LAUNCHERS" ],
[ "ROCKET_LAUNCHERS" ],
[ "FLAMETHROWERS" ]
]
],
"mod_exclusion_category": [ ]
}
]
4 changes: 3 additions & 1 deletion doc/src/content/docs/en/mod/json/reference/json_info.md
Original file line number Diff line number Diff line change
Expand Up @@ -1930,8 +1930,10 @@ Gun mods can be defined like this:
... // Same entries as above for the generic item.
// Additionally some gunmod specific entries:
"location": "stock", // Mandatory. Where is this gunmod is installed?
"mod_targets": [ "crossbow" ], // Mandatory. What specific weapons can this gunmod be used with?
"mod_targets": [ "crossbow" ], // Optional. What specific weapons can this gunmod be used with?
"mod_target_category": [ [ "BOWS" ] ], // Optional. What specific weapon categories can this gunmod be used with?
"mod_exclusions": [ "laser_rifle" ], // Optional. What specific weapons can't this gunmod be used with?
"mod_exclusion_category": [ [ "ENERGY_WEAPONS" ] ], // Optional. What specific weapon categories can't this gunmod be used with?
"acceptable_ammo": [ "9mm" ], // Optional filter restricting mod to guns with those base (before modifiers) ammo types
"install_time": "30 s", // Optional time installation takes. Installation is instantaneous if unspecified. An integer will be read as moves or a time string can be used.
"ammo_modifier": [ "57" ], // Optional field which if specified modifies parent gun to use these ammo types
Expand Down
54 changes: 51 additions & 3 deletions src/item.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -2575,6 +2575,33 @@ void item::gunmod_info( std::vector<iteminfo> &info, const iteminfo_query *parts
info.emplace_back( "GUNMOD", used_on_str );
}

if( !( mod.exclusion.empty() && mod.exclusion_category.empty() ) &&
parts->test( iteminfo_parts::GUNMOD_EXCLUSION ) ) {
std::string exclusion_str = _( "<bold>Cannot be used on:</bold>" );

if( !mod.exclusion.empty() ) {
exclusion_str += _( "\n Specific: " ) + enumerate_as_string( mod.exclusion.begin(),
mod.exclusion.end(), []( const itype_id & excluded ) {
return string_format( "<info>%s</info>", excluded->nname( 1 ) );
} );
}

if( !mod.exclusion_category.empty() ) {
exclusion_str += _( "\n Category: " );
std::vector<std::string> combination;
combination.reserve( mod.exclusion_category.size() );
for( const std::unordered_set<weapon_category_id> &catgroup : mod.exclusion_category ) {
combination.emplace_back( ( "[" ) + enumerate_as_string( catgroup.begin(),
catgroup.end(), []( const weapon_category_id & wcid ) {
return string_format( "<info>%s</info>", wcid->name().translated() );
}, enumeration_conjunction::none ) + ( "]" ) );
}
exclusion_str += enumerate_as_string( combination, enumeration_conjunction::or_ );
}

info.emplace_back( "GUNMOD", exclusion_str );
}

if( parts->test( iteminfo_parts::GUNMOD_LOCATION ) ) {
info.emplace_back( "GUNMOD", string_format( _( "Location: %s" ),
mod.location.name() ) );
Expand Down Expand Up @@ -7799,10 +7826,31 @@ ret_val<bool> item::is_gunmod_compatible( const item &mod ) const
return ret_val<bool>::make_failure( _( "doesn't have enough room for another %s mod" ),
mod.type->gunmod->location.name() );

} else if( !g_mod.usable.empty() || !g_mod.usable_category.empty() ) {
} else if( !g_mod.usable.empty() || !g_mod.usable_category.empty() || !g_mod.exclusion.empty() ||
!g_mod.exclusion_category.empty() ) {
// First check that it's not explicitly excluded by id.
bool excluded = g_mod.exclusion.count( this->typeId() );
// Then check if it's excluded by category.
for( const std::unordered_set<weapon_category_id> &mod_cat : g_mod.exclusion_category ) {
if( excluded ) {
break;
}
if( std::all_of( mod_cat.begin(), mod_cat.end(), [this]( const weapon_category_id & wcid ) {
return this->type->weapon_category.count( wcid );
} ) ) {
excluded = true;
}
}

// Check that it's included by id, if so, override banned so it's allowed.
// A check is already in item_factory so that explicit inclusion and exclusion of the same id throws errors.
bool usable = g_mod.usable.count( this->typeId() );
if( usable ) {
excluded = false;
}
// Then check that it's included by category. If banned is still true, skip, no point checking.
for( const std::unordered_set<weapon_category_id> &mod_cat : g_mod.usable_category ) {
if( usable ) {
if( usable || excluded ) {
break;
}
if( std::all_of( mod_cat.begin(), mod_cat.end(), [this]( const weapon_category_id & wcid ) {
Expand All @@ -7811,7 +7859,7 @@ ret_val<bool> item::is_gunmod_compatible( const item &mod ) const
usable = true;
}
}
if( !usable ) {
if( !usable || excluded ) {
return ret_val<bool>::make_failure( _( "cannot have a %s" ), mod.tname() );
}

Expand Down
26 changes: 26 additions & 0 deletions src/item_factory.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1420,13 +1420,37 @@ void Item_factory::check_definitions() const
}
}
}
for( const itype_id &t : type->gunmod->exclusion ) {
if( !t.is_valid() ) {
msg += string_format( "gunmod excludes for invalid item %s\n", t.c_str() );
}
if( type->gunmod->usable.count( t ) ) {
msg += string_format( "gunmod includes and excludes same item %s\n", t.c_str() );
}
}
for( const std::unordered_set<weapon_category_id> &wv : type->gunmod->usable_category ) {
for( const weapon_category_id &wid : wv ) {
if( !wid.is_valid() ) {
msg += string_format( "gunmod is usable for invalid weapon category %s\n", wid.c_str() );
}
}
}
for( const std::unordered_set<weapon_category_id> &wv : type->gunmod->exclusion_category ) {
for( const weapon_category_id &wid : wv ) {
if( !wid.is_valid() ) {
msg += string_format( "gunmod excludes for invalid weapon category %s\n", wid.c_str() );
}
}
for( const std::unordered_set<weapon_category_id> &test_wv : type->gunmod->usable_category ) {
if( wv == test_wv ) {
std::string group_format = ( "[" ) + enumerate_as_string( wv.begin(),
wv.end(), []( const weapon_category_id & wcid ) {
return string_format( "%s", wcid.c_str() );
}, enumeration_conjunction::none ) + ( "]" );
msg += string_format( "gunmod includes and excludes weapon category group %s\n", group_format );
}
}
}
}
if( type->mod ) {
for( const ammotype &at : type->mod->ammo_modifier ) {
Expand Down Expand Up @@ -2379,6 +2403,8 @@ void Item_factory::load( islot_gunmod &slot, const JsonObject &jo, const std::st

assign( jo, "mod_targets", slot.usable );
assign( jo, "mod_target_category", slot.usable_category );
assign( jo, "mod_exclusions", slot.exclusion );
assign( jo, "mod_exclusion_category", slot.exclusion_category );

assign( jo, "mode_modifier", slot.mode_modifier );
assign( jo, "reload_modifier", slot.reload_modifier );
Expand Down
1 change: 1 addition & 0 deletions src/iteminfo_query.h
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,7 @@ enum class iteminfo_parts : size_t {
GUNMOD_ADD_MOD,

GUNMOD_USEDON,
GUNMOD_EXCLUSION,
GUNMOD_LOCATION,
GUNMOD_BLACKLIST_MOD,

Expand Down
2 changes: 2 additions & 0 deletions src/itype.h
Original file line number Diff line number Diff line change
Expand Up @@ -567,6 +567,8 @@ struct islot_gunmod : common_ranged_data {
/** What kind of weapons can this gunmod be used with (e.g. "rifle", "crossbow")? */
std::unordered_set<itype_id> usable;
std::vector<std::unordered_set<weapon_category_id>> usable_category;
std::unordered_set<itype_id> exclusion;
std::vector<std::unordered_set<weapon_category_id>> exclusion_category;

/** If this value is set (non-negative), this gunmod functions as a sight. A sight is only usable to aim by a character whose current @ref Character::recoil is at or below this value. */
int sight_dispersion = -1;
Expand Down

0 comments on commit 85f265a

Please sign in to comment.