diff --git a/data/mods/Xedra_Evolved/monster_special_attacks/monster_special_attacks.json b/data/mods/Xedra_Evolved/monster_special_attacks/monster_special_attacks.json index 83bcec1c92e10..3a6ad70e625ee 100644 --- a/data/mods/Xedra_Evolved/monster_special_attacks/monster_special_attacks.json +++ b/data/mods/Xedra_Evolved/monster_special_attacks/monster_special_attacks.json @@ -61,7 +61,6 @@ "id": "ratatosks_curse", "cooldown": 10, "move_cost": 100, - "attack_chance": 50, "hitsize_min": 4, "attack_upper": false, "damage_max_instance": [ { "damage_type": "bash", "amount": 0 } ], diff --git a/doc/MONSTER_SPECIAL_ATTACKS.md b/doc/MONSTER_SPECIAL_ATTACKS.md index 927e0ca564024..8d7fde14c35d1 100644 --- a/doc/MONSTER_SPECIAL_ATTACKS.md +++ b/doc/MONSTER_SPECIAL_ATTACKS.md @@ -44,7 +44,7 @@ Generally [hardcoded special attacks](#hardcoded-special-attacks) are declared t It contains either: * An `id` of a [hardcoded special attack](#hardcoded-special-attacks), or a [JSON-declared attacks](/data/json/monster_special_attacks). -* A `type` member (string) plus a `cooldown` member (integer) pair, for [partially hardcoded special attacks](#partially-hardcoded-special-attacks). +* A `type` member (string) plus a `cooldown` member (integer or this can be a Variable Object, see the [doc](EFFECT_ON_CONDITION.md) for more info.) pair, for [partially hardcoded special attacks](#partially-hardcoded-special-attacks). * A spell (see [MAGIC.md](MAGIC.md)). Depending on the kind of attack, it may contain additional required members. Example: @@ -178,7 +178,7 @@ These special attacks are defined in [JSON](/data/json/monster_special_attacks), | field | description | --- | --- -| `cooldown` | Integer, amount of turns between uses. +| `cooldown` | Integer or Variable Object, see the [doc](EFFECT_ON_CONDITION.md) for more info, amount of turns between uses. | `damage_max_instance` | Array of objects. See also [MONSTERS.md#melee_damage](MONSTERS.md#melee_damage). | `min_mul`, `max_mul` | Sets the bounds on the range of damage done. For each attack, the above defined amount of damage will be multiplied by a | | randomly rolled multiplier between the values `min_mul` and `max_mul`. Default 0.5 and 1.0, meaning each attack will do at least half of the defined damage. @@ -284,7 +284,7 @@ Casts a separately-defined spell at the monster's target. Spells with `target_s | --- | ------------------------------------------------------------------------------------------------------- | | `spell_data` | List of spell properties for the attack. | | `min_level` | The level at which the spell is cast. Spells cast by monsters do not gain levels like player spells. | -| `cooldown ` | How often the monster can cast this spell. +| `cooldown ` | Integer or a Variable Object, see the [doc](EFFECT_ON_CONDITION.md) for more info. How often the monster can cast this spell. | `monster_message` | Message to print when the spell is cast, replacing the `message` in the spell definition. Dynamic fields correspond to ` / / `. | | `condition` | Object, dialogue conditions enabling the attack. See `NPCs.md` for the possible conditions, `u` refers to the casting monster and `npc` to the target unless the spell allows no target (in which case only self-conditions can be defined). | `allow_no_target` | Bool, default `false`. If `true` the monster will cast it even without a hostile target. | diff --git a/src/mattack_actors.cpp b/src/mattack_actors.cpp index a204e2581bc82..9dad99373b36c 100644 --- a/src/mattack_actors.cpp +++ b/src/mattack_actors.cpp @@ -77,7 +77,6 @@ void leap_actor::load_internal( const JsonObject &obj, const std::string & ) // Optional: min_range = obj.get_float( "min_range", 1.0f ); allow_no_target = obj.get_bool( "allow_no_target", false ); - optional( obj, was_loaded, "attack_chance", attack_chance, 100 ); optional( obj, was_loaded, "prefer_leap", prefer_leap, false ); optional( obj, was_loaded, "random_leap", random_leap, false ); optional( obj, was_loaded, "ignore_dest_terrain", ignore_dest_terrain, false ); @@ -256,7 +255,6 @@ void mon_spellcasting_actor::load_internal( const JsonObject &obj, const std::st //~ " cast on !" to_translation( "%1$s casts %2$s at %3$s!" ) ); spell_data.trigger_message = monster_message; - optional( obj, was_loaded, "attack_chance", attack_chance, 100 ); optional( obj, was_loaded, "allow_no_target", allow_no_target, false ); if( obj.has_member( "condition" ) ) { @@ -353,7 +351,6 @@ void melee_actor::load_internal( const JsonObject &obj, const std::string & ) damage_max_instance = load_damage_instance( obj ); } - optional( obj, was_loaded, "attack_chance", attack_chance, 100 ); optional( obj, was_loaded, "accuracy", accuracy, INT_MIN ); optional( obj, was_loaded, "min_mul", min_mul, 0.5f ); optional( obj, was_loaded, "max_mul", max_mul, 1.0f ); @@ -1160,9 +1157,8 @@ bool gun_actor::call( monster &z ) const for( const auto &e : ranges ) { if( dist >= e.first.first && dist <= e.first.second ) { if( untargeted || try_target( z, *target ) ) { - shoot( z, aim_at, e.second ); + return shoot( z, aim_at, e.second ); } - return true; } } return false; @@ -1216,11 +1212,9 @@ bool gun_actor::try_target( monster &z, Creature &target ) const return true; } -void gun_actor::shoot( monster &z, const tripoint &target, const gun_mode_id &mode, +bool gun_actor::shoot( monster &z, const tripoint &target, const gun_mode_id &mode, int inital_recoil ) const { - z.mod_moves( -move_cost ); - itype_id mig_gun_type = item_controller->migrate_id( gun_type ); item gun( mig_gun_type ); gun.gun_set_mode( mode ); @@ -1246,7 +1240,7 @@ void gun_actor::shoot( monster &z, const tripoint &target, const gun_mode_id &mo if( z.has_effect( effect_stunned ) || z.has_effect( effect_psi_stunned ) || z.has_effect( effect_sensor_stun ) ) { - return; + return false; } add_msg_debug( debugmode::DF_MATTACK, "%d ammo (%s) remaining", z.ammo[ammo_type], @@ -1256,9 +1250,9 @@ void gun_actor::shoot( monster &z, const tripoint &target, const gun_mode_id &mo if( !no_ammo_sound.empty() ) { sounds::sound( z.pos(), 10, sounds::sound_t::combat, no_ammo_sound ); } - return; + return false; } - + z.mod_moves( -move_cost ); standard_npc tmp( _( "The " ) + z.name(), z.pos(), {}, 8, fake_str, fake_dex, fake_int, fake_per ); tmp.worn.wear_item( tmp, item( "backpack" ), false, false, true, true ); @@ -1289,4 +1283,5 @@ void gun_actor::shoot( monster &z, const tripoint &target, const gun_mode_id &mo } else { z.ammo[ammo_type] -= tmp.fire_gun( target, gun.gun_current_mode().qty ); } + return true; } diff --git a/src/mattack_actors.h b/src/mattack_actors.h index dc2eb4bb0c627..9b24a34d62bc0 100644 --- a/src/mattack_actors.h +++ b/src/mattack_actors.h @@ -261,7 +261,7 @@ class gun_actor : public mattack_actor bool require_sunlight = false; bool try_target( monster &z, Creature &target ) const; - void shoot( monster &z, const tripoint &target, const gun_mode_id &mode, + bool shoot( monster &z, const tripoint &target, const gun_mode_id &mode, int inital_recoil = 0 ) const; int get_max_range() const; diff --git a/src/mattack_common.h b/src/mattack_common.h index 75bf28d1d1015..9656a8c597eff 100644 --- a/src/mattack_common.h +++ b/src/mattack_common.h @@ -28,9 +28,7 @@ class mattack_actor mattack_id id; bool was_loaded = false; - int cooldown = 0; - // Percent chance for the attack to happen if the mob tries it - int attack_chance = 100; + dbl_or_var cooldown; // Dialogue conditions of the attack std::function condition; diff --git a/src/mondefense.cpp b/src/mondefense.cpp index b6ecca070fdab..c3e2d35967a5f 100644 --- a/src/mondefense.cpp +++ b/src/mondefense.cpp @@ -190,10 +190,10 @@ void mdefense::return_fire( monster &m, Creature *source, const dealt_projectile continue; } - gunactor->shoot( m, fire_point, gun_mode_DEFAULT, dispersion ); - - // We only return fire once with one gun. - return; + if( gunactor->shoot( m, fire_point, gun_mode_DEFAULT, dispersion ) ) { + // We only return fire once with one gun. + return; + } } } } diff --git a/src/monster.cpp b/src/monster.cpp index 4c1b8d67baa64..984e921ccdeb5 100644 --- a/src/monster.cpp +++ b/src/monster.cpp @@ -291,9 +291,10 @@ monster::monster( const mtype_id &id ) : monster() moves = type->speed; Creature::set_speed_base( type->speed ); hp = type->hp; + dialogue d( get_talker_for( this ), get_talker_for( get_avatar() ) ); for( const auto &sa : type->special_attacks ) { mon_special_attack &entry = special_attacks[sa.first]; - entry.cooldown = rng( 0, sa.second->cooldown ); + entry.cooldown = rng( 0, sa.second->cooldown.evaluate( d ) ); } anger = type->agro; morale = type->morale; @@ -385,9 +386,10 @@ void monster::poly( const mtype_id &id ) morale = type->morale; hp = static_cast( hp_percentage * type->hp ); special_attacks.clear(); + dialogue d( get_talker_for( this ), get_talker_for( get_avatar() ) ); for( const auto &sa : type->special_attacks ) { mon_special_attack &entry = special_attacks[sa.first]; - entry.cooldown = sa.second->cooldown; + entry.cooldown = sa.second->cooldown.evaluate( d ); } faction = type->default_faction; upgrades = type->upgrades; @@ -2585,12 +2587,15 @@ void monster::reset_stats() void monster::reset_special( const std::string &special_name ) { - set_special( special_name, type->special_attacks.at( special_name )->cooldown ); + dialogue d( get_talker_for( this ), get_talker_for( get_avatar() ) ); + set_special( special_name, type->special_attacks.at( special_name )->cooldown.evaluate( d ) ); } void monster::reset_special_rng( const std::string &special_name ) { - set_special( special_name, rng( 0, type->special_attacks.at( special_name )->cooldown ) ); + dialogue d( get_talker_for( this ), get_talker_for( get_avatar() ) ); + set_special( special_name, rng( 0, + type->special_attacks.at( special_name )->cooldown.evaluate( d ) ) ); } void monster::set_special( const std::string &special_name, int time ) diff --git a/src/monstergenerator.cpp b/src/monstergenerator.cpp index 8bd0e3a7ca0be..a25e4867b53be 100644 --- a/src/monstergenerator.cpp +++ b/src/monstergenerator.cpp @@ -508,7 +508,7 @@ mtype MonsterGenerator::generate_fake_pseudo_dormant_monster( const mtype &mon ) // first make a new mon_spellcasting_actor actor std::unique_ptr new_actor( new mon_spellcasting_actor() ); new_actor->allow_no_target = true; - new_actor->cooldown = 1; + new_actor->cooldown.min.dbl_val = 1; new_actor->spell_data.id = spell_pseudo_dormant_trap_setup; new_actor->spell_data.self = true; @@ -1473,7 +1473,7 @@ void mattack_actor::load( const JsonObject &jo, const std::string &src ) assign( jo, "id", id, false ); } - assign( jo, "cooldown", cooldown, strict ); + cooldown = get_dbl_or_var( jo, "cooldown", false, 0.0 ); load_internal( jo, src ); // Set was_loaded manually because we don't have generic_factory to do it for us @@ -1527,7 +1527,14 @@ void mtype::add_special_attack( const JsonArray &inner, const std::string_view ) } } mtype_special_attack new_attack = mtype_special_attack( iter->second ); - new_attack.actor->cooldown = inner.get_int( 1 ); + if( inner.has_array( 1 ) ) { + new_attack.actor->cooldown.min = get_dbl_or_var_part( inner.get_array( 1 )[0], + "special attack cooldown", 0.0 ); + new_attack.actor->cooldown.max = get_dbl_or_var_part( inner.get_array( 1 )[1], + "special attack cooldown", 0.0 ); + } else { + new_attack.actor->cooldown.min = get_dbl_or_var_part( inner[1], "special attack cooldown", 0.0 ); + } special_attacks.emplace( name, new_attack ); special_attacks_names.push_back( name ); } diff --git a/src/savegame_json.cpp b/src/savegame_json.cpp index cf3820a4b6366..3baaccae2fcfd 100644 --- a/src/savegame_json.cpp +++ b/src/savegame_json.cpp @@ -2500,7 +2500,8 @@ void monster::load( const JsonObject &data ) if( ptimeout >= 0 ) { entry.cooldown = ptimeout; } else { // -1 means disabled, unclear what <-1 values mean in old saves - entry.cooldown = type->special_attacks.at( aname )->cooldown; + dialogue d( get_talker_for( this ), get_talker_for( get_avatar() ) ); + entry.cooldown = type->special_attacks.at( aname )->cooldown.evaluate( d ); entry.enabled = false; } } @@ -2523,7 +2524,8 @@ void monster::load( const JsonObject &data ) const std::string &aname = sa.first; if( special_attacks.find( aname ) == special_attacks.end() ) { auto &entry = special_attacks[aname]; - entry.cooldown = rng( 0, sa.second->cooldown ); + dialogue d( get_talker_for( this ), get_talker_for( get_avatar() ) ); + entry.cooldown = rng( 0, sa.second->cooldown.evaluate( d ) ); } }