Skip to content

Commit

Permalink
fail to feed for non-standard mags (#74696)
Browse files Browse the repository at this point in the history
* fail to feed, initial commit

* refine calculations, add field for guns also, add debug tool to damage items

* add jams, apply not a single jam, but random from group

* move quick fixing to aim activity actor

* better calculations for jam chance

* swap fault functions because github do not show them correctly

* mics changes

* Prevent loop of "fixed fail to feed - found the gun failed to feed"

* mics changes

* mics changes

* finishing bits

* document thing

* add manual fault fixes

* Astyle

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>

* Apply suggestions from code review

---------

Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com>
  • Loading branch information
GuardianDll and github-actions[bot] authored Aug 24, 2024
1 parent ae2b99e commit 292302f
Show file tree
Hide file tree
Showing 11 changed files with 304 additions and 25 deletions.
46 changes: 37 additions & 9 deletions data/json/faults/faults_guns.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,14 +7,6 @@
"item_prefix": "rusting",
"flags": [ "BLACKPOWDER_FOULING_DAMAGE", "NO_DIRTYING" ]
},
{
"id": "fault_gun_chamber_spent",
"type": "fault",
"name": { "str": "Spent casing in chamber" },
"description": "This gun currently has an empty casing chambered. It will have to be removed before firing.",
"item_prefix": "jammed",
"flags": [ "JAMMED_GUN" ]
},
{
"id": "fault_gun_unlubricated",
"type": "fault",
Expand Down Expand Up @@ -54,6 +46,42 @@
"type": "fault",
"name": { "str": "Overheat safety" },
"description": "This weapon will attempt to enter a cooling cycle when overheated.",
"flags": [ "JAMMED_GUN" ]
"flags": [ "OVERHEATED_GUN" ]
},
{
"id": "fault_fail_to_feed",
"type": "fault",
"//": "gun_mechanical_simple can be fixed on the fly",
"fault_type": "gun_mechanical_simple",
"name": { "str": "Fail to feed" },
"description": "This gun did not load the round in the chamber properly.",
"item_prefix": "jammed"
},
{
"id": "fault_gun_chamber_spent",
"//": "aka fail to extract",
"type": "fault",
"fault_type": "gun_mechanical_simple",
"name": { "str": "Spent casing in chamber" },
"description": "This gun currently has an empty casing in the chamber.",
"item_prefix": "jammed"
},
{
"id": "fault_stovepipe",
"//": "only closed bolt",
"type": "fault",
"fault_type": "gun_mechanical_simple",
"name": { "str": "Stovepipe" },
"description": "Casing of the bullet stuck without being properly ejected.",
"item_prefix": "jammed"
},
{
"id": "fault_double_feed",
"//": "only magazine-fed firearms",
"type": "fault",
"fault_type": "gun_mechanical_simple",
"name": { "str": "Double feed" },
"description": "Magazine of the gun tried to put two rounds in the chamber at once.",
"item_prefix": "jammed"
}
]
32 changes: 30 additions & 2 deletions data/json/faults/fixes_gun.json
Original file line number Diff line number Diff line change
Expand Up @@ -66,14 +66,42 @@
"time_save_profs": { "prof_gun_cleaning": 0.5 },
"time_save_flags": { "EASY_CLEAN": 0.5 }
},
{
"type": "fault_fix",
"id": "mend_fault_fail_to_feed_manual_feed",
"name": "Manual cycle",
"success_msg": "You manually cycle your %s to put round in the chamber.",
"time": "10 s",
"set_variables": { "u_know_round_in_chamber": "true" },
"faults_removed": [ "fault_fail_to_feed" ]
},
{
"type": "fault_fix",
"id": "mend_fault_gun_chamber_spent_eject",
"name": "Eject spent casing",
"success_msg": "You eject the spent casing from the %s.",
"time": "1 s",
"success_msg": "You eject the spent casing from the %s's chamber.",
"time": "10 s",
"set_variables": { "u_know_round_in_chamber": "true" },
"faults_removed": [ "fault_gun_chamber_spent" ]
},
{
"type": "fault_fix",
"id": "mend_fault_stovepipe_eject",
"name": "Eject spent casing",
"success_msg": "You eject the spent casing, stuck in %s's slide.",
"time": "10 s",
"set_variables": { "u_know_round_in_chamber": "true" },
"faults_removed": [ "fault_stovepipe" ]
},
{
"type": "fault_fix",
"id": "mend_fault_double_feed_clean",
"name": "Clean double feed",
"success_msg": "You eject the second round stuck in %s's chamber.",
"time": "10 s",
"set_variables": { "u_know_round_in_chamber": "true" },
"faults_removed": [ "fault_double_feed" ]
},
{
"type": "fault_fix",
"id": "mend_fault_gun_unlubricated",
Expand Down
65 changes: 65 additions & 0 deletions data/json/items/tool/debug_tools.json
Original file line number Diff line number Diff line change
Expand Up @@ -11,5 +11,70 @@
"symbol": ";",
"color": "light_gray",
"use_action": [ "DBG_LUX_METER" ]
},
{
"id": "debug_item_damager",
"type": "TOOL",
"category": "tools",
"name": { "str_sp": "debug item damager (simple)" },
"description": "Deals 1 full bar of damage to an item you wield.",
"weight": "1 g",
"volume": "1 ml",
"material": [ "plastic" ],
"symbol": ";",
"color": "light_gray",
"use_action": {
"type": "effect_on_conditions",
"menu_text": "Damage item",
"effect_on_conditions": [
{
"id": "EOC_FIND_ITEM_simple",
"effect": {
"u_run_inv_eocs": "all",
"search_data": [ { "wielded_only": true } ],
"true_eocs": {
"id": "EOC_DAMAGE_ITEM_simple",
"effect": [
{ "math": [ "_hp_before", "=", "n_hp('ALL')" ] },
{ "math": [ "n_hp('ALL')", "-=", "1000" ] },
{ "math": [ "_hp_after", "=", "n_hp('ALL')" ] },
{ "u_message": "<npc_name> have had <context_val:hp_before> hp, now has <context_val:hp_after> hp." }
]
}
}
}
]
}
},
{
"id": "debug_item_damager_adv",
"type": "TOOL",
"category": "tools",
"name": { "str_sp": "debug item damager (advanced)" },
"description": "Deals damage to items you pick.",
"weight": "1 g",
"volume": "1 ml",
"material": [ "plastic" ],
"symbol": ";",
"color": "light_gray",
"use_action": {
"type": "effect_on_conditions",
"menu_text": "Damage item",
"effect_on_conditions": [
{
"id": "EOC_FIND_ITEM",
"effect": {
"u_run_inv_eocs": "manual_mult",
"true_eocs": {
"id": "EOC_DAMAGE_ITEM",
"effect": [
{ "math": [ "n_hp('ALL')", "-=", "num_input('Amount of damage, 1000 is one bar of damage.', 1000)" ] },
{ "u_message": "Dealt damage to all items." }
]
}
}
}
]
}
}
]
2 changes: 2 additions & 0 deletions doc/JSON_INFO.md
Original file line number Diff line number Diff line change
Expand Up @@ -3608,6 +3608,7 @@ ammo_effects define what effect the projectile, that you shoot, would have. List
"count" : 0, // Default amount of ammo contained by a magazine (set this for ammo belts)
"default_ammo": "556", // If specified override the default ammo (optionally set this for ammo belts)
"reload_time" : 100, // How long it takes to load each unit of ammo into the magazine
"mag_jam_mult": 1.25 // Multiplier for gun mechanincal malfunctioning from magazine, mostly when it's damaged; Values lesser than 1 reflect better quality of the magazine, that jam less; bigger than 1 result in gun being more prone to malfunction and jam at lesser damage level; zero mag_jam_mult (and zero gun_jam_mult in a gun) would remove any chance for a gun to malfunction. Only works if gun has any fault from gun_mechanical_simple group presented; Jam chances are described in Character::handle_gun_damage(); at this moment it is roughly: 0.000288% for undamaged magazine, 5% for 1 damage (|\), 24% for 2 damage (|.), 96% for 3 damage (\.), and 250% for 4 damage (XX), then this and gun values are summed up and multiplied by 1.8.
"linkage" : "ammolink" // If set one linkage (of given type) is dropped for each unit of ammo consumed (set for disintegrating ammo belts)
```

Expand Down Expand Up @@ -4155,6 +4156,7 @@ Guns can be defined like this:
"sight_dispersion": 10, // Inaccuracy of gun derived from the sight mechanism, measured in 100ths of Minutes Of Angle (MOA)
"recoil": 0, // Recoil caused when firing, measured in 100ths of Minutes Of Angle (MOA)
"durability": 8, // Resistance to damage/rusting, also determines misfire chance
"gun_jam_mult": 1.25 // Multiplier for gun mechanincal malfunctioning, mostly when it's damaged; Values lesser than 1 reflect better quality of the gun, that jam less; bigger than 1 result in gun being more prone to malfunction and jam at lesser damage level; zero gun_jam_mult (and zero mag_jam_mult if magazine is presented) would remove any chance for a gun to malfunction. Only apply if gun has any fault from gun_mechanical_simple group presented; Jam chances are described in Character::handle_gun_damage(); at this moment it is roughly: 0.00018% for undamaged gun, 3% for 1 damage (|\), 15% for 2 damage (|.), 45% for 3 damage (\.), and 80% for 4 damage (XX), then this and magazine values are summed up and multiplied by 1.8
"blackpowder_tolerance": 8,// One in X chance to get clogged up (per shot) when firing blackpowder ammunition (higher is better). Optional, default is 8.
"min_cycle_recoil": 0, // Minimum ammo recoil for gun to be able to fire more than once per attack.
"clip_size": 100, // Maximum amount of ammo that can be loaded
Expand Down
53 changes: 52 additions & 1 deletion src/activity_actor.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@
#include "event_bus.h"
#include "faction.h"
#include "field_type.h"
#include "fault.h"
#include "flag.h"
#include "flexbuffer_json-inl.h"
#include "flexbuffer_json.h"
Expand Down Expand Up @@ -252,6 +253,7 @@ static const quality_id qual_SHEAR( "SHEAR" );
static const skill_id skill_computer( "computer" );
static const skill_id skill_electronics( "electronics" );
static const skill_id skill_fabrication( "fabrication" );
static const skill_id skill_gun( "gun" );
static const skill_id skill_mechanics( "mechanics" );
static const skill_id skill_survival( "survival" );
static const skill_id skill_traps( "traps" );
Expand Down Expand Up @@ -283,6 +285,8 @@ static const zone_type_id zone_type_LOOT_IGNORE_FAVORITES( "LOOT_IGNORE_FAVORITE
static const zone_type_id zone_type_STRIP_CORPSES( "STRIP_CORPSES" );
static const zone_type_id zone_type_UNLOAD_ALL( "UNLOAD_ALL" );

static const std::string gun_mechanical_simple( "gun_mechanical_simple" );

std::string activity_actor::get_progress_message( const player_activity &act ) const
{
if( act.moves_total > 0 ) {
Expand Down Expand Up @@ -317,13 +321,60 @@ aim_activity_actor aim_activity_actor::use_mutation( const item &fake_gun )
return act;
}

void aim_activity_actor::start( player_activity &act, Character &/*who*/ )
void aim_activity_actor::start( player_activity &act, Character &who )
{
item_location weapon = get_weapon();
item &it = *weapon.get_item();

if( !check_gun_ability_to_shoot( who, it ) ) {
aborted = true; // why doesn't interrupt?
act.set_to_null();
}

// Time spent on aiming is determined on the go by the player
act.moves_total = 1;
act.moves_left = 1;
}

bool aim_activity_actor::check_gun_ability_to_shoot( Character &who, item &it )
{

if( it.has_fault_flag( "RUINED_GUN" ) ) {
who.add_msg_if_player( m_bad, _( "Your %s is little more than an awkward club now." ), it.tname() );
return false;
}

// if it's a simple fault, character can try to fix it on the fly
if( faults::random_of_type_item_has( it, gun_mechanical_simple ) != fault_id::NULL_ID() ) {
// fixing fault should cost more than 1 second
// but until game running the next activity actor without ever verifying
// was the previous one successful or not will be resolved,
// it would be safer to limit it somewhat
who.mod_moves( -who.get_speed() );
who.recoil = MAX_RECOIL;
if( one_in( std::max( 7.0f, ( 15.0f - ( 4.0f * who.get_skill_level( skill_gun ) ) ) ) ) ) {
who.add_msg_if_player( m_good,
_( "Your %s has some mechanical malfunction. You tried to quickly fix it, and it works now!" ),
it.tname() );
it.faults.erase( faults::random_of_type_item_has( it, gun_mechanical_simple ) );
it.set_var( "u_know_round_in_chamber", true );
} else {
who.add_msg_if_player( m_bad,
_( "Your %s has some mechanical malfunction. You tried to quickly fix it, but failed!" ),
it.tname() );
return false;
}
}

if( it.has_fault_flag( "OVERHEATED_GUN" ) ) {
who.add_msg_if_player( m_warning,
_( "Your %s is too hot, and little screen signalizes the gun is inoperable." ), it.tname() );
return false;
}

return true;
}

void aim_activity_actor::do_turn( player_activity &act, Character &who )
{
if( !who.is_avatar() ) {
Expand Down
1 change: 1 addition & 0 deletions src/activity_actor_definitions.h
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,7 @@ class aim_activity_actor : public activity_actor
}

void start( player_activity &act, Character &who ) override;
bool check_gun_ability_to_shoot( Character &who, item &it );
void do_turn( player_activity &act, Character &who ) override;
void finish( player_activity &act, Character &who ) override;
void canceled( player_activity &act, Character &who ) override;
Expand Down
35 changes: 35 additions & 0 deletions src/fault.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,41 @@ const fault_id &faults::random_of_type( const std::string &type )
return random_entry_ref( typed->second );
}

const fault_id &faults::random_of_type_item_has( const item &it, const std::string &type )
{
const auto &typed = faults_by_type.find( type );
if( typed == faults_by_type.end() ) {
debugmsg( "there are no faults with type '%s'", type );
return fault_id::NULL_ID();
}

// not actually random
for( const fault_id &fid : typed->second ) {
if( it.has_fault( fid ) ) {
return fid;
}
}

return fault_id::NULL_ID();
}

const fault_id &faults::get_random_of_type_item_can_have( const item &it, const std::string &type )
{
const auto &typed = faults_by_type.find( type );
if( typed == faults_by_type.end() ) {
debugmsg( "there are no faults with type '%s'", type );
return fault_id::NULL_ID();
}

for( const fault_id &fid : typed->second ) {
if( it.faults_potential().count( fid ) ) {
return fid;
}
}

return fault_id::NULL_ID();
}

void faults::load_fault( const JsonObject &jo, const std::string &src )
{
fault_factory.load( jo, src );
Expand Down
3 changes: 3 additions & 0 deletions src/fault.h
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,10 @@ void reset();
void finalize();
void check_consistency();

const fault_id &get_random_of_type_item_can_have( const item &it, const std::string &type );

const fault_id &random_of_type( const std::string &type );
const fault_id &random_of_type_item_has( const item &it, const std::string &type );
} // namespace faults

class fault_fix
Expand Down
2 changes: 2 additions & 0 deletions src/item_factory.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -2839,6 +2839,7 @@ void Item_factory::load( islot_gun &slot, const JsonObject &jo, const std::strin
assign( jo, "heat_per_shot", slot.heat_per_shot, strict, 0.0 );
assign( jo, "cooling_value", slot.cooling_value, strict, 0.0 );
assign( jo, "overheat_threshold", slot.overheat_threshold, strict, -1.0 );
optional( jo, false, "gun_jam_mult", slot.gun_jam_mult, 1 );

if( jo.has_array( "valid_mod_locations" ) ) {
slot.valid_mod_locations.clear();
Expand Down Expand Up @@ -3573,6 +3574,7 @@ void Item_factory::load( islot_magazine &slot, const JsonObject &jo, const std::
assign( jo, "count", slot.count, strict, 0 );
assign( jo, "default_ammo", slot.default_ammo, strict );
assign( jo, "reload_time", slot.reload_time, strict, 0 );
optional( jo, false, "mag_jam_mult", slot.mag_jam_mult, 1 );
assign( jo, "linkage", slot.linkage, strict );
}

Expand Down
8 changes: 8 additions & 0 deletions src/itype.h
Original file line number Diff line number Diff line change
Expand Up @@ -787,6 +787,11 @@ struct islot_gun : common_ranged_data {
*/
double overheat_threshold = -1.0;

/**
* Multiplier of the chance for the gun to jam.
*/
double gun_jam_mult = 1;

std::map<ammotype, std::set<itype_id>> cached_ammos;

/**
Expand Down Expand Up @@ -948,6 +953,9 @@ struct islot_magazine {
/** How long it takes to load each unit of ammo into the magazine */
int reload_time = 100;

/** Multiplier for the gun jamming from physical damage */
double mag_jam_mult = 1 ;

/** For ammo belts one linkage (of given type) is dropped for each unit of ammo consumed */
std::optional<itype_id> linkage;

Expand Down
Loading

0 comments on commit 292302f

Please sign in to comment.