diff --git a/data/json/monstergroups/zombies.json b/data/json/monstergroups/zombies.json index ef2aaf2202e90..1886067195f6f 100644 --- a/data/json/monstergroups/zombies.json +++ b/data/json/monstergroups/zombies.json @@ -1007,7 +1007,8 @@ "//": "Corpses buried at a mass grave. No ferals, and few animals.", "default": "mon_zombie", "monsters": [ - { "monster": "mon_zombie", "weight": 264, "cost_multiplier": 0 }, + { "monster": "mon_zombie", "weight": 132, "cost_multiplier": 0 }, + { "monster": "mon_pseudo_dormant_zombie", "weight": 132, "cost_multiplier": 0 }, { "monster": "mon_zombie_fat", "weight": 266, "cost_multiplier": 0 }, { "monster": "mon_zombie_child", "weight": 100, "cost_multiplier": 0 }, { "monster": "mon_zombie_tough", "weight": 100, "cost_multiplier": 0 }, diff --git a/data/json/monsters/monster_flags.json b/data/json/monsters/monster_flags.json index 05c193a507932..2627d94d661af 100644 --- a/data/json/monsters/monster_flags.json +++ b/data/json/monsters/monster_flags.json @@ -289,6 +289,16 @@ "type": "monster_flag", "//": "Monster corpse will revive after a short period of time" }, + { + "id": "DORMANT", + "type": "monster_flag", + "//": "Monster corpse can be revived by dormant corpse traps" + }, + { + "id": "QUIETDEATH", + "type": "monster_flag", + "//": "Monster dies quietly and doesn't display a message." + }, { "id": "VERMIN", "type": "monster_flag", diff --git a/data/json/monsters/zed_dormant.json b/data/json/monsters/zed_dormant.json new file mode 100644 index 0000000000000..3cff0f32f6df1 --- /dev/null +++ b/data/json/monsters/zed_dormant.json @@ -0,0 +1,110 @@ +[ + { + "type": "SPELL", + "id": "kill_pseudo_zombie", + "name": { "str": "kill pseudo zombie" }, + "description": "You should not see this. Kills pseudo zombie to turn it into a corpse for revival.", + "valid_targets": [ "self" ], + "max_level": 1, + "flags": [ "SILENT", "NO_EXPLOSION_SFX", "PERCENTAGE_DAMAGE", "NO_FAIL" ], + "base_casting_time": 1, + "shape": "blast", + "min_damage": 100, + "max_damage": 100, + "min_aoe": 1, + "max_aoe": 1, + "message": "", + "effect": "attack", + "damage_type": "pure" + }, + { + "type": "SPELL", + "id": "pseudo_dormant_trap_setup", + "name": { "str": "dormant corpse setup" }, + "description": "You should not see this. Sets up trap for dormant zombies.", + "valid_targets": [ "self" ], + "max_level": 1, + "flags": [ "SILENT", "NO_EXPLOSION_SFX" ], + "base_casting_time": 1, + "shape": "blast", + "min_aoe": 1, + "max_aoe": 1, + "message": "", + "effect": "add_trap", + "effect_str": "tr_dormant_corpse", + "extra_effects": [ { "id": "kill_pseudo_zombie", "hit_self": true } ] + }, + { + "id": "mon_pseudo_dormant_zombie", + "type": "MONSTER", + "name": { "str": "zombie" }, + "description": "Fake zombie used for spawning dormant zombies. If you see this, open an issue on github.", + "copy-from": "mon_zombie", + "looks_like": "corpse_mon_zombie", + "hp": 5, + "speed": 1, + "flags": [ "FILTHY", "REVIVES", "DORMANT", "QUIETDEATH" ], + "zombify_into": "mon_zombie", + "special_attacks": [ + { + "id": "pseudo_dormant_trap_setup_attk", + "type": "spell", + "spell_data": { "id": "pseudo_dormant_trap_setup", "hit_self": true }, + "cooldown": 1, + "allow_no_target": true, + "monster_message": "" + } + ] + }, + { + "type": "trap", + "id": "tr_dormant_corpse", + "name": "dormant zombie corpse", + "//": "This zombie 'corpse' looks eerily like it could get up and start walking at any moment.", + "color": "light_green", + "symbol": "Z", + "visibility": 8, + "avoidance": 20, + "difficulty": 99, + "flags": [ "UNDODGEABLE", "AVATAR_ONLY", "PROXIMITY" ], + "action": "spell", + "spell_data": { "id": "wake_up_dormant" }, + "trigger_message_u": "A zombie rises from the ground!", + "trigger_message_npc": "A zombie rises from the ground!", + "sound_threshold": [ 10, 20 ] + }, + { + "type": "SPELL", + "id": "cause_loud_moaning", + "name": { "str": "cause loud moaning" }, + "description": "You should not see this. Causes loud noise.", + "valid_targets": [ "ground", "ally" ], + "max_level": 1, + "shape": "blast", + "effect": "noise", + "flags": [ "NO_EXPLOSION_SFX" ], + "sound_type": "combat", + "sound_id": "melee_attack", + "sound_variant": "monster_melee_hit", + "sound_description": "a zombie moaning!", + "min_damage": 60, + "max_damage": 60, + "//": "volume is damage/3, so 20" + }, + { + "type": "SPELL", + "id": "wake_up_dormant", + "name": { "str": "dormant corpse waking up" }, + "description": "You should not see this. Causes all dormant corpses in the tile to revive.", + "valid_targets": [ "ground" ], + "max_level": 1, + "flags": [ "NO_EXPLOSION_SFX" ], + "base_casting_time": 1, + "shape": "blast", + "min_aoe": 1, + "max_aoe": 1, + "effect": "revive_dormant", + "effect_str": "ZOMBIE", + "extra_effects": [ { "id": "cause_loud_moaning" } ] + } +] diff --git a/data/json/traps.json b/data/json/traps.json index ba23e165df55e..9766db24cc231 100644 --- a/data/json/traps.json +++ b/data/json/traps.json @@ -1166,6 +1166,6 @@ "avoidance": 8, "difficulty": 0, "action": "sound_detect", - "sound_threshold": 5 + "sound_threshold": [ 15, 25 ] } ] diff --git a/doc/JSON_FLAGS.md b/doc/JSON_FLAGS.md index f32ace92d5874..4ba03c05367d1 100644 --- a/doc/JSON_FLAGS.md +++ b/doc/JSON_FLAGS.md @@ -1137,6 +1137,7 @@ Other monster flags. - ```COMBAT_MOUNT``` This mount has better chance to ignore hostile monster fear - ```CONSOLE_DESPAWN``` Despawns when a nearby console is properly hacked. - ```CONVERSATION``` This monster can engage in conversation. Will need to have chat_topics as well. +- ```DORMANT``` This monster will be revived by dormant corpse traps. - ```DEADLY_VIRUS``` This monster can inflict the zombie_virus effect - ```DESTROYS``` Bashes down walls and more. (2.5x bash multiplier, where base is the critter's max melee bashing) - ```DIGS``` Digs through the ground. Will not travel through non-diggable terrain such as roads. diff --git a/doc/JSON_INFO.md b/doc/JSON_INFO.md index c35e86983c503..2ccd779fd0875 100644 --- a/doc/JSON_INFO.md +++ b/doc/JSON_INFO.md @@ -3035,7 +3035,7 @@ See [MUTATIONS.md](MUTATIONS.md) }, "trigger_message_u": "A bear trap closes on your foot!", // This message will be printed when player steps on a trap "trigger_message_npc": "A bear trap closes on 's foot!", // This message will be printed when NPC or monster steps on a trap - "sound_threshold": 5 // Optional. Minimum volume of sound that will trigger this trap. Defaults to 0 (Will not trigger from sound). + "sound_threshold": [5,10] // Optional. Minimum volume of sound that will trigger this trap. Defaults to [0,0] (Will not trigger from sound). If two values [min,max] are provided, trap triggers on a linearly increasing chance depending on volume, from 25% (min) to 100%(max). To always trigger at some noise, say noise level N, specify as [N,N]. IMPORTANT: Not all traps work with this system. Make sure to double check and test. ``` ### Vehicle Groups diff --git a/doc/MAGIC.md b/doc/MAGIC.md index bffa162e00091..49a179f356343 100644 --- a/doc/MAGIC.md +++ b/doc/MAGIC.md @@ -163,6 +163,7 @@ Below is a table of currently implemented effects, along with special rules for Effect | Description --- |--- +`add_trap` | Adds a trap in the target tile. This always succeeds (unless there is an existing trap) and only places 1 trap. The `effect_str` is the id of the trap. `area_pull` | Pulls `valid_targets` in its aoe toward the target location. Currently, the pull distance is set to 1 (see `directed_push`). `area_push` | Pushes `valid_targets` in its aoe away from the target location. Currently, the push distance is set to 1 (see `directed_push`). `attack` | Causes damage to `valid_targets` in its aoe, and applies `effect_str` named effect to targets. To damage terrain use `bash`. @@ -189,6 +190,7 @@ Effect | Description `remove_effect` | Removes `effect_str` effects from all creatures in the aoe. `remove_field` | Removes a `effect_str` field in the aoe. Causes teleglow of varying intensity and potentially teleportation depending on field density, if the field removed is `fd_fatigue`. `revive` | Revives a monster like a zombie necromancer. The monster must have the `REVIVES` flag. +`revive_dormant` | Revives a dormant monster. The monster must have the `REVIVES` AND the `DORMANT` flag. `short_range_teleport` | Teleports the player randomly range spaces with aoe variation. See also the `TARGET_TELEPORT` and `UNSAFE_TELEPORT` flags. `slime_split` | The slime splits into two large or normal slimes, depending on mass. Note: hardcoded for `mon_blob`-type enemies, check the monster `death_function` + spell `summon` combination. `spawn_item` | Spawns an item that will disappear at the end of its duration. Default duration is 0. diff --git a/src/game.cpp b/src/game.cpp index 98bbc21ec777d..835f1f0fd266c 100644 --- a/src/game.cpp +++ b/src/game.cpp @@ -5443,6 +5443,11 @@ bool game::is_sheltered( const tripoint &p ) } bool game::revive_corpse( const tripoint &p, item &it ) +{ + return revive_corpse( p, it, 1 ); +} + +bool game::revive_corpse( const tripoint &p, item &it, int radius ) { if( !it.is_corpse() ) { debugmsg( "Tried to revive a non-corpse." ); @@ -5481,7 +5486,7 @@ bool game::revive_corpse( const tripoint &p, item &it ) } } - return place_critter_at( newmon_ptr, p ); + return place_critter_around( newmon_ptr, p, radius ); } void game::save_cyborg( item *cyborg, const tripoint &couch_pos, Character &installer ) diff --git a/src/game.h b/src/game.h index 5c82e1abbb8bc..c25584ec7b1e6 100644 --- a/src/game.h +++ b/src/game.h @@ -510,6 +510,8 @@ class game * If reviving failed, the item is unchanged, as is the environment (no new monsters). */ bool revive_corpse( const tripoint &p, item &it ); + // same as above, but with relaxed placement radius. + bool revive_corpse( const tripoint &p, item &it, int radius ); /**Turns Broken Cyborg monster into Cyborg NPC via surgery*/ void save_cyborg( item *cyborg, const tripoint &couch_pos, Character &installer ); /** Asks if the player wants to cancel their activity, and if so cancels it. */ diff --git a/src/item.cpp b/src/item.cpp index 2628de9400331..e2d63e42fa999 100644 --- a/src/item.cpp +++ b/src/item.cpp @@ -101,6 +101,7 @@ #include "string_id_utils.h" #include "text_snippets.h" #include "translations.h" +#include "trap.h" #include "try_parse_integer.h" #include "units.h" #include "units_fwd.h" @@ -13032,6 +13033,19 @@ bool item::process_corpse( map &here, Character *carrier, const tripoint &pos ) return false; } + if( corpse->has_flag( mon_flag_DORMANT ) ) { + //if dormant, ensure trap still exists. + const trap *trap_here = &here.tr_at( pos ); + if( trap_here->is_null() ) { + // if there isn't a trap, we need to add one again. + here.trap_set( pos, trap_id( "tr_dormant_corpse" ) ); + } else if( trap_here->loadid != trap_id( "tr_dormant_corpse" ) ) { + // if there is a trap, but it isn't the right one, we need to revive the zombie manually. + return g->revive_corpse( pos, *this, 3 ); + } + return false; + } + // handle human corpses rising as zombies if( corpse->id == mtype_id::NULL_ID() && !has_var( "zombie_form" ) && !mon_human->zombify_into.is_empty() ) { diff --git a/src/magic.cpp b/src/magic.cpp index d015f3170af13..a574f89fd385a 100644 --- a/src/magic.cpp +++ b/src/magic.cpp @@ -3015,6 +3015,13 @@ spell fake_spell::get_spell( const Creature &caster, int min_level_override ) co return sp; } +// intended for spells without casters +spell fake_spell::get_spell() const +{ + spell sp( id ); + return sp; +} + void spell_events::notify( const cata::event &e ) { switch( e.type() ) { diff --git a/src/magic.h b/src/magic.h index 35b1034676589..f9493ea81c7e6 100644 --- a/src/magic.h +++ b/src/magic.h @@ -179,6 +179,7 @@ struct fake_spell { // gets the spell with an additional override for minimum level (default 0) spell get_spell( const Creature &caster, int min_level_override = 0 ) const; + spell get_spell() const; bool is_valid() const; void load( const JsonObject &jo ); @@ -788,6 +789,9 @@ void dash( const spell &sp, Creature &caster, const tripoint &target ); void banishment( const spell &sp, Creature &caster, const tripoint &target ); // revives a monster into some kind of zombie if the monster has the revives flag void revive( const spell &sp, Creature &caster, const tripoint &target ); +// revives a dormant monster if it has the revives and the dormant flag +void revive_dormant( const spell &sp, Creature &caster, const tripoint &target ); +void add_trap( const spell &sp, Creature &caster, const tripoint &target ); void upgrade( const spell &sp, Creature &caster, const tripoint &target ); // causes guilt to the target as if it killed the caster void guilt( const spell &sp, Creature &caster, const tripoint &target ); @@ -811,6 +815,7 @@ std::maphas_flag( mon_flag_REVIVES ) && mt->in_species( spec ) && + mt->has_flag( mon_flag_REVIVES ) && !mt->has_flag( mon_flag_DORMANT ) && mt->in_species( spec ) && !mt->has_flag( mon_flag_NO_NECRO ) ) ) { continue; } @@ -1450,6 +1450,37 @@ void spell_effect::revive( const spell &sp, Creature &caster, const tripoint &ta } } +// identical to above, but checks for REVIVES && DORMANT flag. Ignores NO_NECRO. +void spell_effect::revive_dormant( const spell &sp, Creature &caster, const tripoint &target ) +{ + const std::set area = spell_effect_area( sp, target, caster ); + ::map &here = get_map(); + const species_id spec( sp.effect_data() ); + for( const tripoint &aoe : area ) { + for( item &corpse : here.i_at( aoe ) ) { + const mtype *mt = corpse.get_mtype(); + if( !( corpse.is_corpse() && corpse.can_revive() && corpse.active && + mt->has_flag( mon_flag_REVIVES ) && mt->has_flag( mon_flag_DORMANT ) && mt->in_species( spec ) ) ) { + continue; + } + // relaxed revive with radius. + if( g->revive_corpse( aoe, corpse, 3 ) ) { + here.i_rem( aoe, &corpse ); + break; + } + } + } +} + +void spell_effect::add_trap( const spell &sp, Creature &, const tripoint &target ) +{ + ::map &here = get_map(); + const trap_id tr_id( sp.effect_data() ); + if( here.tr_at( target ) == tr_null ) { + here.trap_set( target, tr_id ); + } +} + void spell_effect::upgrade( const spell &sp, Creature &caster, const tripoint &target ) { const std::set area = spell_effect_area( sp, target, caster ); @@ -1752,7 +1783,8 @@ void spell_effect::banishment( const spell &sp, Creature &caster, const tripoint } } -void spell_effect::effect_on_condition( const spell &sp, Creature &caster, const tripoint &target ) +void spell_effect::effect_on_condition( const spell &sp, Creature &caster, + const tripoint &target ) { const std::set area = spell_effect_area( sp, target, caster ); @@ -1773,7 +1805,8 @@ void spell_effect::effect_on_condition( const spell &sp, Creature &caster, const } } -void spell_effect::slime_split_on_death( const spell &sp, Creature &caster, const tripoint &target ) +void spell_effect::slime_split_on_death( const spell &sp, Creature &caster, + const tripoint &target ) { sp.make_sound( target, caster ); int mass = caster.get_speed_base(); diff --git a/src/map.cpp b/src/map.cpp index 1a10ebacebf14..5b8fdfc4e48d3 100644 --- a/src/map.cpp +++ b/src/map.cpp @@ -130,6 +130,7 @@ static const field_type_str_id field_fd_clairvoyant( "fd_clairvoyant" ); static const flag_id json_flag_AVATAR_ONLY( "AVATAR_ONLY" ); static const flag_id json_flag_PRESERVE_SPAWN_OMT( "PRESERVE_SPAWN_OMT" ); +static const flag_id json_flag_PROXIMITY( "PROXIMITY" ); static const flag_id json_flag_UNDODGEABLE( "UNDODGEABLE" ); static const item_group_id Item_spawn_data_default_zombie_clothes( "default_zombie_clothes" ); @@ -9703,7 +9704,62 @@ void map::creature_on_trap( Creature &c, const bool may_avoid ) const if( you != nullptr && you->in_vehicle ) { return; } - maybe_trigger_trap( c.pos(), c, may_avoid ); + + tripoint pos = c.pos(); + // proximity traps + std::vector tr_proximity; + // find proximity traps in adjacent tiles + for( int x = pos.x - 1; x <= pos.x + 1; x++ ) { + for( int y = pos.y - 1; y <= pos.y + 1; y++ ) { + if( x == pos.x && y == pos.y ) { + continue; + } + const tripoint loc = tripoint( x, y, pos.z ); + const trap *trap_here = &tr_at( loc ); + if( trap_here->has_flag( json_flag_PROXIMITY ) ) { + tr_proximity.push_back( loc ); + } + } + } + // first trigger proximity traps + for( auto &loc : tr_proximity ) { + maybe_trigger_prox_trap( loc, c, may_avoid ); + } + // then traps we stepped on + maybe_trigger_trap( pos, c, may_avoid ); +} + + +void map::maybe_trigger_prox_trap( const tripoint &pos, Creature &c, const bool may_avoid ) const +{ + const trap &tr = tr_at( pos ); + if( tr.is_null() ) { + return; + } + + //Don't trigger benign traps like cots and funnels + if( tr.is_benign() ) { + return; + } + + if( tr.has_flag( json_flag_AVATAR_ONLY ) && !c.is_avatar() ) { + return; + } + + if( !tr.has_flag( json_flag_UNDODGEABLE ) && may_avoid && c.avoid_trap( pos, tr ) ) { + Character *const pl = c.as_character(); + if( !tr.is_always_invisible() && pl && !pl->knows_trap( pos ) ) { + pl->add_msg_if_player( _( "You've spotted a %1$s!" ), tr.name() ); + pl->add_known_trap( pos, tr ); + } + return; + } + + if( !tr.is_always_invisible() && tr.has_trigger_msg() ) { + c.add_msg_player_or_npc( m_bad, tr.get_trigger_message_u(), tr.get_trigger_message_npc(), + tr.name() ); + } + tr.trigger( pos, c ); } void map::maybe_trigger_trap( const tripoint &pos, Creature &c, const bool may_avoid ) const diff --git a/src/map.h b/src/map.h index e95b9e013cd56..83517a17a32e9 100644 --- a/src/map.h +++ b/src/map.h @@ -1510,9 +1510,12 @@ class map * This functions assumes the character is either on top of the trap, * or adjacent to it. */ + // TODO: fix point types (remove the first overload) void maybe_trigger_trap( const tripoint &pos, Creature &c, bool may_avoid ) const; void maybe_trigger_trap( const tripoint_bub_ms &pos, Creature &c, bool may_avoid ) const; + // Handles triggering a proximity trap. Similar but subtly different. + void maybe_trigger_prox_trap( const tripoint &pos, Creature &c, bool may_avoid ) const; // Spawns byproducts from items destroyed in fire. void create_burnproducts( const tripoint &p, const item &fuel, const units::mass &burned_mass ); diff --git a/src/monattack.cpp b/src/monattack.cpp index 46fa59a3140d6..05a1a375fa5ba 100644 --- a/src/monattack.cpp +++ b/src/monattack.cpp @@ -1238,6 +1238,10 @@ bool mattack::resurrect( monster *z ) // Did we successfully raise something? if( g->revive_corpse( raised.first, *raised.second ) ) { here.i_rem( raised.first, raised.second ); + // check to ensure that we destroy any dormant zombie traps in the same tile. + if( here.tr_at( raised.first ) == trap_id( "tr_dormant_corpse" ) ) { + here.remove_trap( raised.first ); + } if( sees_necromancer ) { add_msg( m_info, _( "You feel a strange pulse of energy from the %s." ), z->name() ); } diff --git a/src/mondeath.cpp b/src/mondeath.cpp index 24b5467300568..6bb6e70936c65 100644 --- a/src/mondeath.cpp +++ b/src/mondeath.cpp @@ -62,7 +62,7 @@ item_location mdeath::normal( monster &z ) return {}; } - if( !z.quiet_death ) { + if( !z.quiet_death && !z.has_flag( mon_flag_QUIETDEATH ) ) { if( z.type->in_species( species_ZOMBIE ) ) { sfx::play_variant_sound( "mon_death", "zombie_death", sfx::get_heard_volume( z.pos() ) ); } diff --git a/src/monster.cpp b/src/monster.cpp index 406216849bc11..fa20c350bf79a 100644 --- a/src/monster.cpp +++ b/src/monster.cpp @@ -173,7 +173,6 @@ static const species_id species_ROBOT( "ROBOT" ); static const species_id species_ZOMBIE( "ZOMBIE" ); static const species_id species_nether_player_hate( "nether_player_hate" ); - static const ter_str_id ter_t_gas_pump( "t_gas_pump" ); static const ter_str_id ter_t_gas_pump_a( "t_gas_pump_a" ); @@ -3866,7 +3865,7 @@ void monster::on_load() // TODO: regen_morale float regen = type->regenerates; if( regen <= 0 ) { - if( has_flag( mon_flag_REVIVES ) ) { + if( has_flag( mon_flag_REVIVES ) && !has_flag( mon_flag_DORMANT ) ) { regen = 0.02f * type->hp / to_turns( 1_hours ); } else if( made_of( material_flesh ) || made_of( material_iflesh ) || made_of( material_veggy ) ) { diff --git a/src/monstergenerator.cpp b/src/monstergenerator.cpp index 4514e7819960b..d283e6e30dd48 100644 --- a/src/monstergenerator.cpp +++ b/src/monstergenerator.cpp @@ -887,6 +887,7 @@ void mtype::load( const JsonObject &jo, const std::string &src ) optional( jo, was_loaded, "zombify_into", zombify_into, string_id_reader<::mtype> {}, mtype_id() ); + optional( jo, was_loaded, "fungalize_into", fungalize_into, string_id_reader<::mtype> {}, mtype_id() ); diff --git a/src/mtype.cpp b/src/mtype.cpp index a61cb4120381a..054135f44809f 100644 --- a/src/mtype.cpp +++ b/src/mtype.cpp @@ -68,6 +68,7 @@ mon_flag_id mon_flag_ACIDPROOF, mon_flag_DESTROYS, mon_flag_DIGS, mon_flag_DOGFOOD, + mon_flag_DORMANT, mon_flag_DRIPS_GASOLINE, mon_flag_DRIPS_NAPALM, mon_flag_DROPS_AMMO, @@ -126,6 +127,7 @@ mon_flag_id mon_flag_ACIDPROOF, mon_flag_PUSH_MON, mon_flag_PUSH_VEH, mon_flag_QUEEN, + mon_flag_QUIETDEATH, mon_flag_RANGED_ATTACKER, mon_flag_REVIVES, mon_flag_REVIVES_HEALTHY, @@ -183,6 +185,7 @@ void set_mon_flag_ids() mon_flag_DESTROYS = mon_flag_id( "DESTROYS" ); mon_flag_DIGS = mon_flag_id( "DIGS" ); mon_flag_DOGFOOD = mon_flag_id( "DOGFOOD" ); + mon_flag_DORMANT = mon_flag_id( "DORMANT" ); mon_flag_DRIPS_GASOLINE = mon_flag_id( "DRIPS_GASOLINE" ); mon_flag_DRIPS_NAPALM = mon_flag_id( "DRIPS_NAPALM" ); mon_flag_DROPS_AMMO = mon_flag_id( "DROPS_AMMO" ); @@ -241,6 +244,7 @@ void set_mon_flag_ids() mon_flag_PUSH_MON = mon_flag_id( "PUSH_MON" ); mon_flag_PUSH_VEH = mon_flag_id( "PUSH_VEH" ); mon_flag_QUEEN = mon_flag_id( "QUEEN" ); + mon_flag_QUIETDEATH = mon_flag_id( "QUIETDEATH" ); mon_flag_RANGED_ATTACKER = mon_flag_id( "RANGED_ATTACKER" ); mon_flag_REVIVES = mon_flag_id( "REVIVES" ); mon_flag_REVIVES_HEALTHY = mon_flag_id( "REVIVES_HEALTHY" ); diff --git a/src/mtype.h b/src/mtype.h index c8810146f4f66..6b4e7b2f7cd1f 100644 --- a/src/mtype.h +++ b/src/mtype.h @@ -108,6 +108,7 @@ extern mon_flag_id mon_flag_ACIDPROOF, mon_flag_DESTROYS, mon_flag_DIGS, mon_flag_DOGFOOD, + mon_flag_DORMANT, mon_flag_DRIPS_GASOLINE, mon_flag_DRIPS_NAPALM, mon_flag_DROPS_AMMO, @@ -166,6 +167,7 @@ extern mon_flag_id mon_flag_ACIDPROOF, mon_flag_PUSH_MON, mon_flag_PUSH_VEH, mon_flag_QUEEN, + mon_flag_QUIETDEATH, mon_flag_RANGED_ATTACKER, mon_flag_REVIVES, mon_flag_REVIVES_HEALTHY, diff --git a/src/sounds.cpp b/src/sounds.cpp index 3022cb56f1fab..48f747f62fe99 100644 --- a/src/sounds.cpp +++ b/src/sounds.cpp @@ -482,13 +482,16 @@ void sounds::process_sounds() critter.hear_sound( source, vol, dist, this_centroid.provocative ); } } - // Trigger sound-triggered traps + // Trigger sound-triggered traps and ensure they are still valid for( const trap *trapType : trap::get_sound_triggered_traps() ) { for( const tripoint &tp : get_map().trap_locations( trapType->id ) ) { const int dist = sound_distance( source, tp ); const trap &tr = get_map().tr_at( tp ); - if( tr.triggered_by_sound( vol, dist ) ) { - tr.trigger( tp ); + // Exclude traps that certainly won't hear the sound + if( vol * 2 > dist ) { + if( tr.triggered_by_sound( vol, dist ) ) { + tr.trigger( tp ); + } } } } diff --git a/src/trap.cpp b/src/trap.cpp index a03bf0a7251e7..f795f8b83c5d1 100644 --- a/src/trap.cpp +++ b/src/trap.cpp @@ -369,13 +369,27 @@ bool trap::is_funnel() const bool trap::has_sound_trigger() const { - return !is_null() && sound_threshold > 0; + const bool has_sound_thresh = sound_threshold.first > 0 && sound_threshold.second > 0; + return !is_null() && has_sound_thresh; } bool trap::triggered_by_sound( int vol, int dist ) const { const int volume = vol - dist; - return !is_null() && volume >= sound_threshold; + // now determine sound threshold probabilities + // linear model: 0% below sound_min, 25% at sound_min, 100% at sound_max + const int sound_min = sound_threshold.first; + const int sound_max = sound_threshold.second; + const int sound_range = sound_max - sound_min; + if( volume < sound_min ) { + return false; + } + int sound_chance = 100; + if( sound_range > 0 ) { + sound_chance = 25 + ( 75 * ( volume - sound_min ) / sound_range ); + } + //debugmsg("Sound chance: %d%%", sound_chance); + return !is_null() && ( rng( 0, 100 ) <= sound_chance ); } void trap::on_disarmed( map &m, const tripoint &p ) const diff --git a/src/trap.h b/src/trap.h index ceb57d6164536..d435046120b8e 100644 --- a/src/trap.h +++ b/src/trap.h @@ -153,9 +153,9 @@ struct trap { */ units::mass trigger_weight = 500_gram; /** - * If a sound of at least this volume reaches the trap, it triggers. + * Determines how much sound is needed to trigger the trap. Defined as {min,max}. */ - int sound_threshold = 0; + std::pair sound_threshold = {0, 0}; int funnel_radius_mm = 0; // For disassembly? std::vector> components; diff --git a/src/trapfunc.cpp b/src/trapfunc.cpp index 0adaf669f4d7e..2ae8f8506c015 100644 --- a/src/trapfunc.cpp +++ b/src/trapfunc.cpp @@ -59,6 +59,7 @@ static const efftype_id effect_slimed( "slimed" ); static const efftype_id effect_tetanus( "tetanus" ); static const flag_id json_flag_LEVITATION( "LEVITATION" ); +static const flag_id json_flag_PROXIMITY( "PROXIMITY" ); static const flag_id json_flag_UNCONSUMED( "UNCONSUMED" ); static const itype_id itype_bullwhip( "bullwhip" ); @@ -1527,20 +1528,52 @@ bool trapfunc::drain( const tripoint &, Creature *c, item * ) bool trapfunc::cast_spell( const tripoint &p, Creature *critter, item * ) { if( critter == nullptr ) { - return false; - } - map &here = get_map(); - trap tr = here.tr_at( p ); - const spell trap_spell = tr.spell_data.get_spell( *critter, 0 ); - npc dummy; - if( !tr.has_flag( json_flag_UNCONSUMED ) ) { - here.remove_trap( p ); + map &here = get_map(); + trap tr = here.tr_at( p ); + const spell trap_spell = tr.spell_data.get_spell(); + npc dummy; + if( !tr.has_flag( json_flag_UNCONSUMED ) ) { + here.remove_trap( p ); + } + if( tr.has_flag( json_flag_PROXIMITY ) ) { + // remove all traps in 3-3 area area + for( int x = p.x - 1; x <= p.x + 1; x++ ) { + for( int y = p.y - 1; y <= p.y + 1; y++ ) { + tripoint pt( x, y, p.z ); + if( here.tr_at( pt ).loadid == tr.loadid ) { + here.remove_trap( pt ); + } + } + } + } + // we remove the trap before casting the spell because otherwise if we teleport we might be elsewhere at the end and p is no longer valid + trap_spell.cast_all_effects( dummy, p ); + trap_spell.make_sound( p, get_player_character() ); + return true; + } else { + map &here = get_map(); + trap tr = here.tr_at( p ); + const spell trap_spell = tr.spell_data.get_spell( *critter, 0 ); + npc dummy; + if( !tr.has_flag( json_flag_UNCONSUMED ) ) { + here.remove_trap( p ); + } + if( tr.has_flag( json_flag_PROXIMITY ) ) { + // remove all traps in 3-3 area area + for( int x = p.x - 1; x <= p.x + 1; x++ ) { + for( int y = p.y - 1; y <= p.y + 1; y++ ) { + tripoint pt( x, y, p.z ); + if( here.tr_at( pt ).loadid == tr.loadid ) { + here.remove_trap( pt ); + } + } + } + } + // we remove the trap before casting the spell because otherwise if we teleport we might be elsewhere at the end and p is no longer valid + trap_spell.cast_all_effects( dummy, critter->pos() ); + trap_spell.make_sound( p, get_player_character() ); + return true; } - // we remove the trap before casting the spell because otherwise if we teleport we might be elsewhere at the end and p is no longer valid - trap_spell.cast_all_effects( dummy, critter->pos() ); - trap_spell.make_sound( p, get_player_character() ); - - return true; } bool trapfunc::snake( const tripoint &p, Creature *, item * )