Skip to content

Commit

Permalink
feat: Basic framework for multiple attacks per weapon (cataclysmbntea…
Browse files Browse the repository at this point in the history
…m#5072)

* tmp

* Structure for attack statblock

* Working test case for attack picking

* Docs

* style(autofix.ci): automated formatting

* Tidy

---------

Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
Co-authored-by: Chaosvolt <chaosvolt@users.noreply.github.com>
  • Loading branch information
3 people authored Aug 13, 2024
1 parent ce5fd78 commit 20b1983
Showing 21 changed files with 498 additions and 111 deletions.
18 changes: 18 additions & 0 deletions data/mods/TEST_DATA/monsters.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
[
{
"id": "mon_test_bash",
"copy-from": "debug_mon",
"type": "MONSTER",
"name": { "str": "unbashable monster" },
"description": "This monster exists only for testing purposes.",
"armor_bash": 1000
},
{
"id": "mon_test_stab",
"copy-from": "debug_mon",
"type": "MONSTER",
"name": { "str": "unstabbable monster" },
"description": "This monster exists only for testing purposes.",
"armor_stab": 1000
}
]
25 changes: 25 additions & 0 deletions data/mods/TEST_DATA/weapons.json
Original file line number Diff line number Diff line change
@@ -25,5 +25,30 @@
"volume": "1 L",
"symbol": ";",
"weapon_category": [ "TEST_CAT1" ]
},
{
"id": "test_lucern_hammer",
"type": "GENERIC",
"category": "weapons",
"weapon_category": [ "2H_HAMMERS", "SPEARS", "HOOKED_POLES" ],
"symbol": "/",
"color": "light_gray",
"name": { "str": "two attack lucerne hammer" },
"description": "This Lucerne hammer has been modified to make it hit harder, at the expense of being unable to bash and stab in the same attack.",
"price": "500 USD",
"material": [ "wood", "steel" ],
"flags": [ "DURABLE_MELEE", "REACH_ATTACK", "NONCONDUCTIVE", "POLEARM", "SHEATH_SPEAR", "SPEAR", "ALWAYS_TWOHAND" ],
"techniques": [ "WBLOCK_1", "WIDE", "SWEEP" ],
"weight": "3200 g",
"volume": "3750 ml",
"bashing": 1,
"cutting": 1,
"to_hit": 1,
"attacks": [
{ "id": "BASH", "to_hit": 1, "damage": { "values": [ { "damage_type": "bash", "amount": 50 } ] } },
{ "id": "THRUST", "damage": { "values": [ { "damage_type": "stab", "amount": 45 } ] } }
],
"price_postapoc": "100 USD",
"qualities": [ [ "HAMMER", 2 ], [ "COOK", 1 ] ]
}
]
19 changes: 18 additions & 1 deletion doc/src/content/docs/en/mod/json/reference/json_info.md
Original file line number Diff line number Diff line change
@@ -1457,6 +1457,12 @@ See also VEHICLE_JSON.md
"cutting": 0, // (Optional, default = 0) Cutting damage caused by using it as a melee weapon. This value cannot be negative.
"bashing": 0, // (Optional, default = 0) Bashing damage caused by using it as a melee weapon. This value cannot be negative.
"to_hit": 0, // (Optional, default = 0) To-hit bonus if using it as a melee weapon (whatever for?)
"attacks": [ // (Optional) New attack statblock, WIP feature
{ "id": "BASH", // ID of the attack. Attack with ID "DEFAULT" will be replaced by calculated data (this can be used to remove custom attacks on "copy-from" item)
"to_hit": 1, // To-hit bonus of this attack
"damage": { "values": [ { "damage_type": "bash", "amount": 50 } ] } }, // Damage of this attack, using `damage_instance` syntax (see below)
{ "id": "THRUST", "damage": { "values": [ { "damage_type": "stab", "amount": 45 } ] } }
],
"flags": ["VARSIZE"], // Indicates special effects, see JSON_FLAGS.md
"environmental_protection_with_filter": 6, // the resistance to environmental effects if an item (for example a gas mask) requires a filter to operate and this filter is installed. Used in combination with use_action 'GASMASK' and 'DIVE_TANK'
"magazine_well": 0, // Volume above which the magazine starts to protrude from the item and add extra volume
@@ -1474,7 +1480,7 @@ See also VEHICLE_JSON.md
"radius": 8, // Radius of the explosion. 0 means only the epicenter is affected.
"fire": true, // Should the explosion leave fire
"fragment": { // Projectile data of "shrapnel". This projectile will hit every target in its range and field of view exactly once.
"damage": { // Damage data of the shrapnel projectile.
"damage": { // Damage data of the shrapnel projectile. Uses damage_instance syntax (see below)
"damage_type": "acid", // Type of damage dealt.
"amount": 10 // Amount of damage dealt.
"armor_penetration": 4 // Amount of armor ignored. Applied per armor piece, not in total.
@@ -1484,6 +1490,17 @@ See also VEHICLE_JSON.md
},
```

#### damage_instance

```json
{
"damage_type": "acid", // Type of damage dealt.
"amount": 10, // Amount of damage dealt.
"armor_penetration": 4, // Amount of armor ignored. Applied per armor piece, not in total.
"armor_multiplier": 2.5 // Multiplies remaining damage reduction from armor, applied after armor penetration (if present). Higher numbers make armor more effective at protecting from this attack, lower numbers act as a percentage reduction in remaining armor.
}
```

### Ammo

```json
1 change: 0 additions & 1 deletion src/catalua_bindings_creature.cpp
Original file line number Diff line number Diff line change
@@ -66,7 +66,6 @@ void cata::detail::reg_creature( sol::state &lua )
SET_FX_T( as_character, Character * () );
SET_FX_T( as_avatar, avatar * () );

SET_FX_T( hit_roll, float() const );
SET_FX_T( dodge_roll, float() );
SET_FX_T( stability_roll, float() const );

20 changes: 5 additions & 15 deletions src/character.h
Original file line number Diff line number Diff line change
@@ -645,28 +645,18 @@ class Character : public Creature, public location_visitable<Character>
std::vector<special_attack> mutation_attacks( Creature &t ) const;
/** Returns the bonus bashing damage the player deals based on their stats */
float bonus_damage( bool random ) const;
/** Returns weapon skill */
float get_melee_hit_base() const;
/** Everything up to, but excluding, final roll of @ref hit_roll */
float get_melee_hit( const item &weapon, const attack_statblock &attack ) const;
/** Returns the player's basic hit roll that is compared to the target's dodge roll */
float hit_roll() const override;
float hit_roll( const item &weapon, const attack_statblock &attack ) const;
/** Returns the chance to critical given a hit roll and target's dodge roll */
double crit_chance( float roll_hit, float target_dodge, const item &weap ) const;
/** Returns true if the player scores a critical hit */
bool scored_crit( float target_dodge, const item &weap ) const;
bool scored_crit( float target_dodge, const item &weap, const attack_statblock &attack ) const;
/** Returns cost (in moves) of attacking with given item (no modifiers, like stuck) */
int attack_cost( const item &weap ) const;
/** Gets melee accuracy component from weapon+skills */
float get_hit_weapon( const item &weap ) const;

// If average == true, adds expected values of random rolls instead of rolling.
/** Adds all 3 types of physical damage to instance */
void roll_all_damage( bool crit, damage_instance &di, bool average, const item &weap ) const;
/** Adds player's total bash damage to the damage instance */
void roll_bash_damage( bool crit, damage_instance &di, bool average, const item &weap ) const;
/** Adds player's total cut damage to the damage instance */
void roll_cut_damage( bool crit, damage_instance &di, bool average, const item &weap ) const;
/** Adds player's total stab damage to the damage instance */
void roll_stab_damage( bool crit, damage_instance &di, bool average, const item &weap ) const;
float get_hit_weapon( const item &weap, const attack_statblock &attack ) const;

private:
/** Check if an area-of-effect technique has valid targets */
4 changes: 3 additions & 1 deletion src/character_display.cpp
Original file line number Diff line number Diff line change
@@ -19,6 +19,7 @@
#include "effect.h"
#include "game.h"
#include "input.h"
#include "melee.h"
#include "mutation.h"
#include "options.h"
#include "output.h"
@@ -439,7 +440,8 @@ static void draw_stats_info( const catacurses::window &w_info, const Character &
_( "Dexterity affects your chance to hit in melee combat, helps you steady your "
"gun for ranged combat, and enhances many actions that require finesse." ) );
print_colored_text( w_info, point( 1, 3 ), col_temp, c_light_gray,
string_format( _( "Melee to-hit bonus: <color_white>%+.1lf</color>" ), you.get_melee_hit_base() ) );
string_format( _( "Melee to-hit bonus: <color_white>%+.1lf</color>" ),
you.get_melee_hit( you.used_weapon(), melee::default_attack( you.used_weapon() ) ) ) );
print_colored_text( w_info, point( 1, 4 ), col_temp, c_light_gray,
string_format( _( "Ranged penalty: <color_white>%+d</color>" ),
-std::abs( you.ranged_dex_mod() ) ) );
2 changes: 0 additions & 2 deletions src/creature.h
Original file line number Diff line number Diff line change
@@ -264,8 +264,6 @@ class Creature
/** Empty function. Should always be overwritten by the appropriate player/NPC/monster version. */
virtual void die( Creature *killer ) = 0;

/** Should always be overwritten by the appropriate player/NPC/monster version. */
virtual float hit_roll() const = 0;
virtual float dodge_roll() = 0;
virtual float stability_roll() const = 0;

12 changes: 1 addition & 11 deletions src/damage.cpp
Original file line number Diff line number Diff line change
@@ -197,17 +197,7 @@ resistances::resistances( const item &armor, bool to_self )
}
}
}
resistances::resistances( monster &monster ) : resistances()
{
set_resist( DT_BASH, monster.type->armor_bash );
set_resist( DT_CUT, monster.type->armor_cut );
set_resist( DT_STAB, monster.type->armor_stab );
set_resist( DT_BULLET, monster.type->armor_bullet );
set_resist( DT_ACID, monster.type->armor_acid );
set_resist( DT_HEAT, monster.type->armor_fire );
set_resist( DT_COLD, monster.type->armor_cold );
set_resist( DT_ELECTRIC, monster.type->armor_electric );
}

void resistances::set_resist( damage_type dt, float amount )
{
flat[dt] = amount;
1 change: 0 additions & 1 deletion src/damage.h
Original file line number Diff line number Diff line change
@@ -105,7 +105,6 @@ struct resistances {

// If to_self is true, we want armor's own resistance, not one it provides to wearer
resistances( const item &armor, bool to_self = false );
resistances( monster &monster );
void set_resist( damage_type dt, float amount );
float type_resist( damage_type dt ) const;

111 changes: 105 additions & 6 deletions src/item.cpp
Original file line number Diff line number Diff line change
@@ -69,6 +69,7 @@
#include "map.h"
#include "martialarts.h"
#include "material.h"
#include "melee.h"
#include "messages.h"
#include "mod_manager.h"
#include "monster.h"
@@ -1488,7 +1489,8 @@ static const double hits_by_accuracy[41] = {
double item::effective_dps( const player &guy, const monster &mon ) const
{
const float mon_dodge = mon.get_dodge();
float base_hit = guy.get_dex() / 4.0f + guy.get_hit_weapon( *this );
// TODO: Handle multiple attacks
float base_hit = guy.get_dex() / 4.0f + guy.get_hit_weapon( *this, melee::default_attack( *this ) );
base_hit *= std::max( 0.25f, 1.0f - guy.encumb( bp_torso ) / 100.0f );
float mon_defense = mon_dodge + mon.size_melee_penalty() / 5.0;
constexpr double hit_trials = 10000.0;
@@ -1519,12 +1521,14 @@ double item::effective_dps( const player &guy, const monster &mon ) const
double num_hits = num_all_hits - num_crits;
// sum average damage past armor and return the number of moves required to achieve
// that damage
// @todo Update for attack_statblock
const attack_statblock default_attack = melee::default_attack( *this );
const auto calc_effective_damage = [ &, moves_per_attack]( const double num_strikes,
const bool crit, const player & guy, const monster & mon ) {
monster temp_mon( mon );
double subtotal_damage = 0;
damage_instance base_damage;
guy.roll_all_damage( crit, base_damage, true, *this );
melee::roll_all_damage( guy, crit, base_damage, true, *this, default_attack );
damage_instance dealt_damage = base_damage;
temp_mon.absorb_hit( bodypart_id( "torso" ), dealt_damage );
dealt_damage_instance dealt_dams;
@@ -1543,7 +1547,7 @@ double item::effective_dps( const player &guy, const monster &mon ) const
if( has_technique( rapid_strike ) ) {
monster temp_rs_mon( mon );
damage_instance rs_base_damage;
guy.roll_all_damage( crit, rs_base_damage, true, *this );
melee::roll_all_damage( guy, crit, rs_base_damage, true, *this, default_attack );
damage_instance dealt_rs_damage = rs_base_damage;
for( damage_unit &dmg_unit : dealt_rs_damage.damage_units ) {
dmg_unit.damage_multiplier *= 0.66;
@@ -3547,10 +3551,11 @@ void item::combat_info( std::vector<iteminfo> &info, const iteminfo_query *parts
}

if( ( dmg_bash || dmg_cut || dmg_stab || type->m_to_hit > 0 ) || debug_mode ) {
const attack_statblock &default_attack = melee::default_attack( *this );
damage_instance non_crit;
you.roll_all_damage( false, non_crit, true, *this );
melee::roll_all_damage( you, false, non_crit, true, *this, default_attack );
damage_instance crit;
you.roll_all_damage( true, crit, true, *this );
melee::roll_all_damage( you, true, crit, true, *this, default_attack );
int attack_cost = you.attack_cost( *this );
insert_separation_line( info );
if( parts->test( iteminfo_parts::DESCRIPTION_MELEEDMG ) ) {
@@ -5292,14 +5297,19 @@ int item::attack_cost() const
}

int item::damage_melee( damage_type dt ) const
{
return damage_melee( melee::default_attack( *this ), dt );
}

int item::damage_melee( const attack_statblock &attack, damage_type dt ) const
{
assert( dt >= DT_NULL && dt < NUM_DT );
if( is_null() ) {
return 0;
}

// effectiveness is reduced by 10% per damage level
int res = type->melee[ dt ];
int res = attack.damage.type_damage( dt );
res -= res * std::max( damage_level( 4 ), 0 ) * 0.1;

// apply type specific flags
@@ -5321,6 +5331,7 @@ int item::damage_melee( damage_type dt ) const
break;
}

// @todo: This probably breaks attack_statblock logic completely...
// consider any melee gunmods
if( is_gun() ) {
const std::vector<const item *> &mods = gunmods();
@@ -5347,6 +5358,94 @@ int item::damage_melee( damage_type dt ) const
return std::max( res, 0 );
}

std::map<std::string, attack_statblock> item::get_attacks() const
{
if( is_null() ) {
return {{"DEFAULT", attack_statblock{}}};
}

std::map<std::string, attack_statblock> result;

// TODO: Cache
for( const auto &attack : type->attacks ) {
attack_statblock modified_attack = attack.second;
for( damage_unit &du : modified_attack.damage.damage_units ) {
// effectiveness is reduced by 10% per damage level
du.amount -= du.amount * std::max( damage_level( 4 ), 0 ) * 0.1;
// apply type specific flags
switch( du.type ) {
case DT_BASH:
if( has_flag( flag_REDUCED_BASHING ) ) {
du.amount *= 0.5;
}
break;

case DT_CUT:
case DT_STAB:
if( has_flag( flag_DIAMOND ) ) {
du.amount *= 1.3;
}
break;

default:
break;
}

switch( du.type ) {
case DT_BASH:
du.amount += bonus_from_enchantments_wielded( du.amount, enchant_vals::mod::ITEM_DAMAGE_BASH,
true );
break;
case DT_CUT:
du.amount += bonus_from_enchantments_wielded( du.amount, enchant_vals::mod::ITEM_DAMAGE_CUT,
true );
break;
case DT_STAB:
du.amount += bonus_from_enchantments_wielded( du.amount, enchant_vals::mod::ITEM_DAMAGE_STAB,
true );
break;
default:
break;
}
}
result[attack.first] = modified_attack;

}

// consider any melee gunmods
if( is_gun() ) {
// TODO: Multiple bayonets with multiple attacks each - add all attacks, resolve id conflicts
const std::vector<const item *> &mods = gunmods();
float best_damage = 0.0f;
const attack_statblock *best = nullptr;
for( const item *gunmod_ptr : mods ) {
const item &gunmod = *gunmod_ptr;
if( gunmod.has_flag( flag_MELEE_GUNMOD ) ) {
// TODO: Handle multiple attacks here - add all of them as separate attacks
assert( !gunmod.type->attacks.empty() );
const attack_statblock &first_attack = gunmod.type->attacks.begin()->second;
float damage_sum = std::accumulate( first_attack.damage.begin(), first_attack.damage.end(),
0.0f, []( float amount_sum,
const damage_unit & du ) {
// Ignore multipliers for now because it's a temporary hack
return amount_sum + du.amount;
} );
if( damage_sum > best_damage ) {
best = &first_attack;
best_damage = damage_sum;
}
}
}
if( best != nullptr ) {
attack_statblock gunmod_attack = *best;
gunmod_attack.to_hit = type->m_to_hit;
result["BAYONET"] = gunmod_attack;
}
}

return result;
}

damage_instance item::base_damage_melee() const
{
// TODO: Caching
4 changes: 4 additions & 0 deletions src/item.h
Original file line number Diff line number Diff line change
@@ -62,6 +62,7 @@ struct tripoint;
template<typename T>
class ret_val;
class item_location;
struct attack_statblock;

namespace enchant_vals
{
@@ -638,6 +639,9 @@ class item : public location_visitable<item>, public game_object<item>

/** Damage of given type caused when this item is used as melee weapon */
int damage_melee( damage_type dt ) const;
int damage_melee( const attack_statblock &attack, damage_type dt ) const;
/** Gets @ref itype::attacks, modified by this item modifiers (gunmods, DIAMOND etc.) */
std::map<std::string, attack_statblock> get_attacks() const;

/** All damage types this item deals when used in melee (no skill modifiers etc. applied). */
damage_instance base_damage_melee() const;
Loading

0 comments on commit 20b1983

Please sign in to comment.