1 #include "mattack_actors.h"
2
3 #include <algorithm>
4 #include <limits>
5 #include <list>
6 #include <memory>
7 #include <string>
8
9 #include "avatar.h"
10 #include "bodypart.h"
11 #include "calendar.h"
12 #include "character.h"
13 #include "creature.h"
14 #include "enums.h"
15 #include "game.h"
16 #include "generic_factory.h"
17 #include "gun_mode.h"
18 #include "item.h"
19 #include "item_pocket.h"
20 #include "json.h"
21 #include "line.h"
22 #include "map.h"
23 #include "map_iterator.h"
24 #include "messages.h"
25 #include "monster.h"
26 #include "npc.h"
27 #include "player.h"
28 #include "point.h"
29 #include "ret_val.h"
30 #include "rng.h"
31 #include "sounds.h"
32 #include "translations.h"
33 #include "viewer.h"
34
35 static const efftype_id effect_badpoison( "badpoison" );
36 static const efftype_id effect_bite( "bite" );
37 static const efftype_id effect_grabbed( "grabbed" );
38 static const efftype_id effect_infected( "infected" );
39 static const efftype_id effect_laserlocked( "laserlocked" );
40 static const efftype_id effect_poison( "poison" );
41 static const efftype_id effect_stunned( "stunned" );
42 static const efftype_id effect_sensor_stun( "sensor_stun" );
43 static const efftype_id effect_targeted( "targeted" );
44 static const efftype_id effect_was_laserlocked( "was_laserlocked" );
45
46 static const trait_id trait_TOXICFLESH( "TOXICFLESH" );
47
load_internal(const JsonObject & obj,const std::string &)48 void leap_actor::load_internal( const JsonObject &obj, const std::string & )
49 {
50 // Mandatory:
51 max_range = obj.get_float( "max_range" );
52 // Optional:
53 min_range = obj.get_float( "min_range", 1.0f );
54 allow_no_target = obj.get_bool( "allow_no_target", false );
55 move_cost = obj.get_int( "move_cost", 150 );
56 min_consider_range = obj.get_float( "min_consider_range", 0.0f );
57 max_consider_range = obj.get_float( "max_consider_range", 200.0f );
58 }
59
clone() const60 std::unique_ptr<mattack_actor> leap_actor::clone() const
61 {
62 return std::make_unique<leap_actor>( *this );
63 }
64
call(monster & z) const65 bool leap_actor::call( monster &z ) const
66 {
67 if( !z.can_act() || !z.move_effects( false ) ) {
68 return false;
69 }
70
71 std::vector<tripoint> options;
72 tripoint target = z.move_target();
73 float best_float = trigdist ? trig_dist( z.pos(), target ) : square_dist( z.pos(), target );
74 if( best_float < min_consider_range || best_float > max_consider_range ) {
75 return false;
76 }
77
78 // We wanted the float for range check
79 // int here will make the jumps more random
80 int best = std::numeric_limits<int>::max();
81 if( !allow_no_target && z.attack_target() == nullptr ) {
82 return false;
83 }
84 map &here = get_map();
85 std::multimap<int, tripoint> candidates;
86 for( const tripoint &candidate : here.points_in_radius( z.pos(), max_range ) ) {
87 if( candidate == z.pos() ) {
88 continue;
89 }
90 float leap_dist = trigdist ? trig_dist( z.pos(), candidate ) :
91 square_dist( z.pos(), candidate );
92 if( leap_dist > max_range || leap_dist < min_range ) {
93 continue;
94 }
95 int candidate_dist = rl_dist( candidate, target );
96 if( candidate_dist >= best_float ) {
97 continue;
98 }
99 candidates.emplace( candidate_dist, candidate );
100 }
101 for( const auto &candidate : candidates ) {
102 const int &cur_dist = candidate.first;
103 const tripoint &dest = candidate.second;
104 if( cur_dist > best ) {
105 break;
106 }
107 if( !z.sees( dest ) ) {
108 continue;
109 }
110 if( !g->is_empty( dest ) ) {
111 continue;
112 }
113 bool blocked_path = false;
114 // check if monster has a clear path to the proposed point
115 std::vector<tripoint> line = here.find_clear_path( z.pos(), dest );
116 for( auto &i : line ) {
117 if( here.impassable( i ) ) {
118 blocked_path = true;
119 break;
120 }
121 }
122 // don't leap into water if you could drown (#38038)
123 if( z.is_aquatic_danger( dest ) ) {
124 blocked_path = true;
125 }
126 if( blocked_path ) {
127 continue;
128 }
129
130 options.push_back( dest );
131 best = cur_dist;
132 }
133
134 if( options.empty() ) {
135 return false; // Nowhere to leap!
136 }
137
138 z.moves -= move_cost;
139 viewer &player_view = get_player_view();
140 const tripoint chosen = random_entry( options );
141 bool seen = player_view.sees( z ); // We can see them jump...
142 z.setpos( chosen );
143 seen |= player_view.sees( z ); // ... or we can see them land
144 if( seen ) {
145 add_msg( _( "The %s leaps!" ), z.name() );
146 }
147
148 return true;
149 }
150
clone() const151 std::unique_ptr<mattack_actor> mon_spellcasting_actor::clone() const
152 {
153 return std::make_unique<mon_spellcasting_actor>( *this );
154 }
155
load_internal(const JsonObject & obj,const std::string &)156 void mon_spellcasting_actor::load_internal( const JsonObject &obj, const std::string & )
157 {
158 mandatory( obj, was_loaded, "spell_data", spell_data );
159 translation monster_message;
160 optional( obj, was_loaded, "monster_message", monster_message,
161 //~ "<Monster Display name> cast <Spell Name> on <Target name>!"
162 to_translation( "%1$s casts %2$s at %3$s!" ) );
163 spell_data.trigger_message = monster_message;
164 }
165
call(monster & mon) const166 bool mon_spellcasting_actor::call( monster &mon ) const
167 {
168 if( !mon.can_act() ) {
169 return false;
170 }
171
172 if( !mon.attack_target() ) {
173 // this is an attack. there is no reason to attack if there isn't a real target.
174 return false;
175 }
176
177 const tripoint target = spell_data.self ? mon.pos() : mon.attack_target()->pos();
178 spell spell_instance = spell_data.get_spell();
179 spell_instance.set_message( spell_data.trigger_message );
180
181 // Bail out if the target is out of range.
182 if( !spell_data.self && rl_dist( mon.pos(), target ) > spell_instance.range() ) {
183 return false;
184 }
185
186 std::string target_name;
187 if( const Creature *target_monster = g->critter_at( target ) ) {
188 target_name = target_monster->disp_name();
189 }
190
191 add_msg_if_player_sees( target, spell_instance.message(), mon.disp_name(),
192 spell_instance.name(), target_name );
193
194 avatar fake_player;
195 mon.moves -= spell_instance.casting_time( fake_player, true );
196 spell_instance.cast_all_effects( mon, target );
197
198 return true;
199 }
200
melee_actor()201 melee_actor::melee_actor()
202 {
203 damage_max_instance = damage_instance::physical( 9, 0, 0, 0 );
204 min_mul = 0.5f;
205 max_mul = 1.0f;
206 move_cost = 100;
207 }
208
load_internal(const JsonObject & obj,const std::string &)209 void melee_actor::load_internal( const JsonObject &obj, const std::string & )
210 {
211 // Optional:
212 if( obj.has_array( "damage_max_instance" ) ) {
213 damage_max_instance = load_damage_instance( obj.get_array( "damage_max_instance" ) );
214 } else if( obj.has_object( "damage_max_instance" ) ) {
215 damage_max_instance = load_damage_instance( obj );
216 }
217
218 min_mul = obj.get_float( "min_mul", 0.0f );
219 max_mul = obj.get_float( "max_mul", 1.0f );
220 move_cost = obj.get_int( "move_cost", 100 );
221 accuracy = obj.get_int( "accuracy", INT_MIN );
222
223 optional( obj, was_loaded, "miss_msg_u", miss_msg_u,
224 to_translation( "The %s lunges at you, but you dodge!" ) );
225 optional( obj, was_loaded, "no_dmg_msg_u", no_dmg_msg_u,
226 to_translation( "The %1$s bites your %2$s, but fails to penetrate armor!" ) );
227 optional( obj, was_loaded, "hit_dmg_u", hit_dmg_u,
228 to_translation( "The %1$s bites your %2$s!" ) );
229 optional( obj, was_loaded, "miss_msg_npc", miss_msg_npc,
230 to_translation( "The %s lunges at <npcname>, but they dodge!" ) );
231 optional( obj, was_loaded, "no_dmg_msg_npc", no_dmg_msg_npc,
232 to_translation( "The %1$s bites <npcname>'s %2$s, but fails to penetrate armor!" ) );
233 optional( obj, was_loaded, "hit_dmg_npc", hit_dmg_npc,
234 to_translation( "The %1$s bites <npcname>'s %2$s!" ) );
235
236 if( obj.has_array( "body_parts" ) ) {
237 for( JsonArray sub : obj.get_array( "body_parts" ) ) {
238 const bodypart_str_id bp( sub.get_string( 0 ) );
239 const float prob = sub.get_float( 1 );
240 body_parts.add_or_replace( bp, prob );
241 }
242 }
243
244 if( obj.has_array( "effects" ) ) {
245 for( JsonObject eff : obj.get_array( "effects" ) ) {
246 effects.push_back( load_mon_effect_data( eff ) );
247 }
248 }
249 }
250
find_target(monster & z) const251 Creature *melee_actor::find_target( monster &z ) const
252 {
253 if( !z.can_act() ) {
254 return nullptr;
255 }
256
257 Creature *target = z.attack_target();
258 if( target == nullptr || !z.is_adjacent( target, false ) ) {
259 return nullptr;
260 }
261
262 return target;
263 }
264
call(monster & z) const265 bool melee_actor::call( monster &z ) const
266 {
267 Creature *target = find_target( z );
268 if( target == nullptr ) {
269 return false;
270 }
271
272 z.mod_moves( -move_cost );
273
274 add_msg_debug( "%s attempting to melee_attack %s", z.name(),
275 target->disp_name() );
276
277 const int acc = accuracy >= 0 ? accuracy : z.type->melee_skill;
278 int hitspread = target->deal_melee_attack( &z, dice( acc, 10 ) );
279
280 if( hitspread < 0 ) {
281 game_message_type msg_type = target->is_avatar() ? m_warning : m_info;
282 sfx::play_variant_sound( "mon_bite", "bite_miss", sfx::get_heard_volume( z.pos() ),
283 sfx::get_heard_angle( z.pos() ) );
284 target->add_msg_player_or_npc( msg_type, miss_msg_u, miss_msg_npc, z.name() );
285 return true;
286 }
287
288 damage_instance damage = damage_max_instance;
289
290 double multiplier = rng_float( min_mul, max_mul );
291 damage.mult_damage( multiplier );
292
293 bodypart_str_id bp_hit = body_parts.empty() ?
294 target->select_body_part( &z, hitspread ).id() :
295 *body_parts.pick();
296
297 target->on_hit( &z, bp_hit.id() );
298 dealt_damage_instance dealt_damage = target->deal_damage( &z, bp_hit.id(), damage );
299 dealt_damage.bp_hit = bp_hit.id();
300
301 int damage_total = dealt_damage.total_damage();
302 add_msg_debug( "%s's melee_attack did %d damage", z.name(), damage_total );
303 if( damage_total > 0 ) {
304 on_damage( z, *target, dealt_damage );
305 } else {
306 sfx::play_variant_sound( "mon_bite", "bite_miss", sfx::get_heard_volume( z.pos() ),
307 sfx::get_heard_angle( z.pos() ) );
308 target->add_msg_player_or_npc( m_neutral, no_dmg_msg_u, no_dmg_msg_npc, z.name(),
309 body_part_name_accusative( bp_hit.id() ) );
310 }
311
312 return true;
313 }
314
on_damage(monster & z,Creature & target,dealt_damage_instance & dealt) const315 void melee_actor::on_damage( monster &z, Creature &target, dealt_damage_instance &dealt ) const
316 {
317 if( target.is_player() ) {
318 sfx::play_variant_sound( "mon_bite", "bite_hit", sfx::get_heard_volume( z.pos() ),
319 sfx::get_heard_angle( z.pos() ) );
320 sfx::do_player_death_hurt( dynamic_cast<player &>( target ), false );
321 }
322 game_message_type msg_type = target.attitude_to( get_player_character() ) ==
323 Creature::Attitude::FRIENDLY ?
324 m_bad : m_neutral;
325 const bodypart_id &bp = dealt.bp_hit ;
326 target.add_msg_player_or_npc( msg_type, hit_dmg_u, hit_dmg_npc, z.name(),
327 body_part_name_accusative( bp ) );
328
329 for( const mon_effect_data &eff : effects ) {
330 if( x_in_y( eff.chance, 100 ) ) {
331 const bodypart_id affected_bp = eff.affect_hit_bp ? bp : eff.bp.id();
332 target.add_effect( eff.id, time_duration::from_turns( eff.duration ), affected_bp, eff.permanent );
333 }
334 }
335 }
336
clone() const337 std::unique_ptr<mattack_actor> melee_actor::clone() const
338 {
339 return std::make_unique<melee_actor>( *this );
340 }
341
342 bite_actor::bite_actor() = default;
343
load_internal(const JsonObject & obj,const std::string & src)344 void bite_actor::load_internal( const JsonObject &obj, const std::string &src )
345 {
346 melee_actor::load_internal( obj, src );
347 no_infection_chance = obj.get_int( "no_infection_chance", 14 );
348 }
349
on_damage(monster & z,Creature & target,dealt_damage_instance & dealt) const350 void bite_actor::on_damage( monster &z, Creature &target, dealt_damage_instance &dealt ) const
351 {
352 melee_actor::on_damage( z, target, dealt );
353 if( target.has_effect( effect_grabbed ) && one_in( no_infection_chance - dealt.total_damage() ) ) {
354 const bodypart_id &hit = dealt.bp_hit;
355 if( target.has_effect( effect_bite, hit.id() ) ) {
356 target.add_effect( effect_bite, 40_minutes, hit, true );
357 } else if( target.has_effect( effect_infected, hit.id() ) ) {
358 target.add_effect( effect_infected, 25_minutes, hit, true );
359 } else {
360 target.add_effect( effect_bite, 1_turns, hit, true );
361 }
362 }
363 if( target.has_trait( trait_TOXICFLESH ) ) {
364 z.add_effect( effect_poison, 5_minutes );
365 z.add_effect( effect_badpoison, 5_minutes );
366 }
367 }
368
clone() const369 std::unique_ptr<mattack_actor> bite_actor::clone() const
370 {
371 return std::make_unique<bite_actor>( *this );
372 }
373
gun_actor()374 gun_actor::gun_actor() : description( to_translation( "The %1$s fires its %2$s!" ) ),
375 targeting_sound( to_translation( "beep-beep-beep!" ) )
376 {
377 }
378
load_internal(const JsonObject & obj,const std::string &)379 void gun_actor::load_internal( const JsonObject &obj, const std::string & )
380 {
381 obj.read( "gun_type", gun_type, true );
382
383 obj.read( "ammo_type", ammo_type );
384
385 if( obj.has_array( "fake_skills" ) ) {
386 for( JsonArray cur : obj.get_array( "fake_skills" ) ) {
387 fake_skills[skill_id( cur.get_string( 0 ) )] = cur.get_int( 1 );
388 }
389 }
390
391 obj.read( "fake_str", fake_str );
392 obj.read( "fake_dex", fake_dex );
393 obj.read( "fake_int", fake_int );
394 obj.read( "fake_per", fake_per );
395
396 for( JsonArray mode : obj.get_array( "ranges" ) ) {
397 if( mode.size() < 2 || mode.get_int( 0 ) > mode.get_int( 1 ) ) {
398 obj.throw_error( "incomplete or invalid range specified", "ranges" );
399 }
400 ranges.emplace( std::make_pair<int, int>( mode.get_int( 0 ), mode.get_int( 1 ) ),
401 gun_mode_id( mode.size() > 2 ? mode.get_string( 2 ) : "" ) );
402 }
403
404 obj.read( "max_ammo", max_ammo );
405
406 obj.read( "move_cost", move_cost );
407
408 obj.read( "description", description );
409 obj.read( "failure_msg", failure_msg );
410 if( !obj.read( "no_ammo_sound", no_ammo_sound ) ) {
411 no_ammo_sound = to_translation( "Click." );
412 }
413
414 obj.read( "targeting_cost", targeting_cost );
415
416 obj.read( "require_targeting_player", require_targeting_player );
417 obj.read( "require_targeting_npc", require_targeting_npc );
418 obj.read( "require_targeting_monster", require_targeting_monster );
419
420 obj.read( "targeting_timeout", targeting_timeout );
421 obj.read( "targeting_timeout_extend", targeting_timeout_extend );
422
423 if( !obj.read( "targeting_sound", targeting_sound ) ) {
424 targeting_sound = to_translation( "Beep." );
425 }
426
427 obj.read( "targeting_volume", targeting_volume );
428
429 laser_lock = obj.get_bool( "laser_lock", false );
430
431 obj.read( "require_sunlight", require_sunlight );
432 }
433
clone() const434 std::unique_ptr<mattack_actor> gun_actor::clone() const
435 {
436 return std::make_unique<gun_actor>( *this );
437 }
438
call(monster & z) const439 bool gun_actor::call( monster &z ) const
440 {
441 Creature *target;
442
443 if( z.friendly ) {
444 int max_range = 0;
445 for( const auto &e : ranges ) {
446 max_range = std::max( std::max( max_range, e.first.first ), e.first.second );
447 }
448
449 int hostiles; // hostiles which cannot be engaged without risking friendly fire
450 target = z.auto_find_hostile_target( max_range, hostiles );
451 if( !target ) {
452 if( hostiles > 0 ) {
453 add_msg_if_player_sees( z, m_warning,
454 ngettext( "Pointed in your direction, the %s emits an IFF warning beep.",
455 "Pointed in your direction, the %s emits %d annoyed sounding beeps.",
456 hostiles ),
457 z.name(), hostiles );
458 }
459 return false;
460 }
461
462 } else {
463 target = z.attack_target();
464 if( !target || !z.sees( *target ) ) {
465 return false;
466 }
467 }
468
469 int dist = rl_dist( z.pos(), target->pos() );
470 for( const auto &e : ranges ) {
471 if( dist >= e.first.first && dist <= e.first.second ) {
472 shoot( z, *target, e.second );
473 return true;
474 }
475 }
476 return false;
477 }
478
shoot(monster & z,Creature & target,const gun_mode_id & mode) const479 void gun_actor::shoot( monster &z, Creature &target, const gun_mode_id &mode ) const
480 {
481 if( require_sunlight && !g->is_in_sunlight( z.pos() ) ) {
482 if( one_in( 3 ) ) {
483 add_msg_if_player_sees( z, failure_msg.translated(), z.name() );
484 }
485 return;
486 }
487
488 const bool require_targeting = ( require_targeting_player && target.is_player() ) ||
489 ( require_targeting_npc && target.is_npc() ) ||
490 ( require_targeting_monster && target.is_monster() );
491 const bool not_targeted = require_targeting && !z.has_effect( effect_targeted );
492 const bool not_laser_locked = require_targeting && laser_lock &&
493 !target.has_effect( effect_was_laserlocked );
494
495 if( not_targeted || not_laser_locked ) {
496 if( targeting_volume > 0 && !targeting_sound.empty() ) {
497 sounds::sound( z.pos(), targeting_volume, sounds::sound_t::alarm,
498 targeting_sound );
499 }
500 if( not_targeted ) {
501 z.add_effect( effect_targeted, time_duration::from_turns( targeting_timeout ) );
502 }
503 if( not_laser_locked ) {
504 target.add_effect( effect_laserlocked, time_duration::from_turns( targeting_timeout ) );
505 target.add_effect( effect_was_laserlocked, time_duration::from_turns( targeting_timeout ) );
506 target.add_msg_if_player( m_warning,
507 _( "You're not sure why you've got a laser dot on you…" ) );
508 }
509
510 z.moves -= targeting_cost;
511 return;
512 }
513
514 z.moves -= move_cost;
515
516 item gun( gun_type );
517 gun.gun_set_mode( mode );
518
519 itype_id ammo = ammo_type;
520 if( ammo.is_null() ) {
521 if( gun.magazine_integral() ) {
522 ammo = gun.ammo_default();
523 } else {
524 ammo = item( gun.magazine_default() ).ammo_default();
525 }
526 }
527
528 if( !ammo.is_null() ) {
529 if( gun.magazine_integral() ) {
530 gun.ammo_set( ammo, z.ammo[ammo] );
531 } else {
532 item mag( gun.magazine_default() );
533 mag.ammo_set( ammo, z.ammo[ammo] );
534 gun.put_in( mag, item_pocket::pocket_type::MAGAZINE_WELL );
535 }
536 }
537
538 if( z.has_effect( effect_stunned ) || z.has_effect( effect_sensor_stun ) ) {
539 return;
540 }
541
542 if( !gun.ammo_sufficient() ) {
543 if( !no_ammo_sound.empty() ) {
544 sounds::sound( z.pos(), 10, sounds::sound_t::combat, no_ammo_sound );
545 }
546 return;
547 }
548
549 standard_npc tmp( _( "The " ) + z.name(), z.pos(), {}, 8,
550 fake_str, fake_dex, fake_int, fake_per );
551 tmp.worn.push_back( item( "backpack" ) );
552 tmp.set_fake( true );
553 tmp.set_attitude( z.friendly ? NPCATT_FOLLOW : NPCATT_KILL );
554 tmp.recoil = 0; // no need to aim
555
556 for( const auto &pr : fake_skills ) {
557 tmp.set_skill_level( pr.first, pr.second );
558 }
559
560 tmp.weapon = gun;
561 tmp.i_add( item( "UPS_off", calendar::turn, 1000 ) );
562
563 add_msg_if_player_sees( z, m_warning, description.translated(), z.name(), tmp.weapon.tname() );
564
565 z.ammo[ammo] -= tmp.fire_gun( target.pos(), gun.gun_current_mode().qty );
566
567 if( require_targeting ) {
568 z.add_effect( effect_targeted, time_duration::from_turns( targeting_timeout_extend ) );
569 }
570
571 if( laser_lock ) {
572 // To prevent spamming laser locks when the player can tank that stuff somehow
573 target.add_effect( effect_was_laserlocked, 5_turns );
574 }
575 }
576