1 /**
2 * @file
3 * @brief Slow projectiles, done as monsters.
4 **/
5
6 #include "AppHdr.h"
7
8 #include "mon-project.h"
9
10 #include <cmath>
11 #include <cstdio>
12 #include <cstdlib>
13 #include <cstring>
14
15 #include "act-iter.h"
16 #include "areas.h"
17 #include "cloud.h"
18 #include "directn.h"
19 #include "env.h"
20 #include "item-prop.h"
21 #include "message.h"
22 #include "mgen-data.h"
23 #include "mon-death.h"
24 #include "mon-place.h"
25 #include "ouch.h"
26 #include "shout.h"
27 #include "stepdown.h"
28 #include "terrain.h"
29 #include "viewchar.h"
30
31 static void _fuzz_direction(const actor *caster, monster& mon, int pow);
32
cast_iood(actor * caster,int pow,bolt * beam,float vx,float vy,int foe,bool fail,bool needs_tracer)33 spret cast_iood(actor *caster, int pow, bolt *beam, float vx, float vy,
34 int foe, bool fail, bool needs_tracer)
35 {
36 const bool is_player = caster->is_player();
37 if (beam && is_player && needs_tracer
38 && !player_tracer(ZAP_IOOD, pow, *beam))
39 {
40 return spret::abort;
41 }
42
43 fail_check();
44
45 int mtarg = !beam ? MHITNOT :
46 beam->target == you.pos() ? int{MHITYOU} : env.mgrid(beam->target);
47
48 monster *mon = place_monster(mgen_data(MONS_ORB_OF_DESTRUCTION,
49 (is_player) ? BEH_FRIENDLY :
50 ((monster*)caster)->friendly() ? BEH_FRIENDLY : BEH_HOSTILE,
51 coord_def(),
52 mtarg).set_summoned(caster, 0, SPELL_IOOD), true, true);
53 if (!mon)
54 {
55 mprf(MSGCH_ERROR, "Failed to spawn projectile.");
56 return spret::abort;
57 }
58
59 if (beam)
60 {
61 beam->choose_ray();
62 #ifdef DEBUG_DIAGNOSTICS
63 const coord_def pos = caster->pos();
64 dprf("beam (%d,%d)+t*(%d,%d) ray (%f,%f)+t*(%f,%f)",
65 pos.x, pos.y, beam->target.x - pos.x, beam->target.y - pos.y,
66 beam->ray.r.start.x - 0.5, beam->ray.r.start.y - 0.5,
67 beam->ray.r.dir.x, beam->ray.r.dir.y);
68 #endif
69 mon->props[IOOD_X].get_float() = beam->ray.r.start.x - 0.5;
70 mon->props[IOOD_Y].get_float() = beam->ray.r.start.y - 0.5;
71 mon->props[IOOD_VX].get_float() = beam->ray.r.dir.x;
72 mon->props[IOOD_VY].get_float() = beam->ray.r.dir.y;
73 _fuzz_direction(caster, *mon, pow);
74 }
75 else
76 {
77 // Multi-orb: spread the orbs a bit, otherwise diagonal ones might
78 // fail to leave the cardinal direction: orb A moves -0.4,+0.9 and
79 // orb B +0.4,+0.9, both rounded to 0,1.
80 mon->props[IOOD_X].get_float() = caster->pos().x + 0.4 * vx;
81 mon->props[IOOD_Y].get_float() = caster->pos().y + 0.4 * vy;
82 mon->props[IOOD_VX].get_float() = vx;
83 mon->props[IOOD_VY].get_float() = vy;
84 }
85
86 mon->props[IOOD_KC].get_byte() = (is_player) ? KC_YOU :
87 ((monster*)caster)->friendly() ? KC_FRIENDLY : KC_OTHER;
88 mon->props[IOOD_POW].get_short() = pow;
89 mon->flags &= ~MF_JUST_SUMMONED;
90 mon->props[IOOD_CASTER].get_string() = caster->as_monster()
91 ? caster->name(DESC_A, true)
92 : (caster->is_player()) ? "you" : "";
93 mon->summoner = caster->mid;
94
95 if (caster->is_player() || caster->type == MONS_PLAYER_GHOST
96 || caster->type == MONS_PLAYER_ILLUSION)
97 {
98 mon->props[IOOD_FLAWED].get_byte() = true;
99 }
100
101 // Move away from the caster's square.
102 iood_act(*mon, true);
103
104 // If the foe was adjacent to the caster, that might have destroyed it.
105 if (mon->alive())
106 {
107 // We need to take at least one full move (for the above), but let's
108 // randomize it and take more so players won't get guaranteed instant
109 // damage.
110 mon->lose_energy(EUT_MOVE, 2, random2(3)+2);
111
112 // Multi-orbs don't home during the first move, they'd likely
113 // immediately explode otherwise.
114 if (foe != MHITNOT)
115 mon->foe = foe;
116 }
117
118 return spret::success;
119 }
120
121 /**
122 * Find a target for a bursty (non-player-targeted) IOOD.
123 *
124 * Try to find an enemy that's at a reasonable angle from the angle the IOOD
125 * is fired at, preferring the given foe (if a non-MHITNOT foe is given) if
126 * they're valid, and otherwise preferring the closest valid foe.
127 *
128 * @param angle The angle that the IOOD will be fired at, relative
129 * to the player's position.
130 * @param preferred_foe The mindex of a target to choose if possible; may
131 * be MHITNOT (no preferred target)
132 * @return The mindex of a valid target for the IOOD.
133 */
_burst_iood_target(double iood_angle,int preferred_foe)134 static int _burst_iood_target(double iood_angle, int preferred_foe)
135 {
136 int closest_foe = MHITNOT;
137 int closest_dist = INT_MAX;
138
139 for (monster_near_iterator mi(you.pos(), LOS_SOLID); mi; ++mi)
140 {
141 const monster* m = *mi;
142 ASSERT(m);
143
144 if (!you.can_see(*m) || mons_is_projectile(*m))
145 continue;
146
147 // is this position at a valid angle?
148 const coord_def delta = mi->pos() - you.pos();
149 const double angle = atan2(delta.x, delta.y);
150 const double abs_angle_diff = abs(angle - fmod(iood_angle, PI * 2));
151 const double angle_diff = (abs_angle_diff > PI) ?
152 2 * PI - abs_angle_diff :
153 abs_angle_diff;
154 if (angle_diff >= PI / 3)
155 {
156 dprf("can't target %s; angle diff %f",
157 m->name(DESC_PLAIN).c_str(), angle_diff);
158 continue;
159 }
160
161 // if preferred foe is valid, choose it.
162 if (m->mindex() == preferred_foe)
163 {
164 dprf("preferred target %s is valid burst target (delta %f)",
165 m->name(DESC_PLAIN).c_str(), angle_diff);
166 return preferred_foe;
167 }
168
169 if (mons_aligned(m, &you) || mons_is_firewood(*m))
170 {
171 dprf("skipping invalid burst target %s (%s)",
172 m->name(DESC_PLAIN).c_str(),
173 mons_aligned(m, &you) ? "aligned" : "firewood");
174 continue;
175 }
176
177 const int dist = grid_distance(m->pos(), you.pos());
178 // on distance ties, bias by iterator order (mindex)
179 if (dist >= closest_dist)
180 {
181 dprf("%s not closer to target than closest (%d vs %d)",
182 m->name(DESC_PLAIN).c_str(), dist, closest_dist);
183 continue;
184 }
185
186 dprf("%s is valid burst target (delta %f, dist %d)",
187 m->name(DESC_PLAIN).c_str(), angle_diff, dist);
188 closest_dist = dist;
189 closest_foe = m->mindex();
190 }
191
192 const int foe = closest_foe != MHITNOT ? closest_foe : preferred_foe;
193 dprf("targeting %d", foe);
194 return foe;
195 }
196
cast_iood_burst(int pow,coord_def target)197 void cast_iood_burst(int pow, coord_def target)
198 {
199 const monster* mons = monster_at(target);
200 const int preferred_foe = mons && you.can_see(*mons) ?
201 mons->mindex() :
202 MHITNOT;
203
204 const int n_orbs = random_range(3, 7);
205 dprf("Bursting %d orbs.", n_orbs);
206 const double angle0 = random2(2097152) * PI * 2 / 2097152;
207
208 for (int i = 0; i < n_orbs; i++)
209 {
210 const double angle = angle0 + i * PI * 2 / n_orbs;
211 const int foe = _burst_iood_target(angle, preferred_foe);
212 cast_iood(&you, pow, 0, sin(angle), cos(angle), foe);
213 }
214 }
215
_normalize(float & x,float & y)216 static void _normalize(float &x, float &y)
217 {
218 const float d = sqrt(x*x + y*y);
219 if (d <= 0.000001)
220 return;
221 x/=d;
222 y/=d;
223 }
224
225 // angle measured in chord length
_in_front(float vx,float vy,float dx,float dy,float angle)226 static bool _in_front(float vx, float vy, float dx, float dy, float angle)
227 {
228 return (dx-vx)*(dx-vx) + (dy-vy)*(dy-vy) <= (angle*angle);
229 }
230
_iood_stop(monster & mon,bool msg=true)231 static void _iood_stop(monster& mon, bool msg = true)
232 {
233 if (!mon.alive())
234 return;
235
236 if (msg)
237 simple_monster_message(mon, " dissipates.");
238 dprf("iood: dissipating");
239 monster_die(mon, KILL_DISMISSED, NON_MONSTER);
240 }
241
_fuzz_direction(const actor * caster,monster & mon,int pow)242 static void _fuzz_direction(const actor *caster, monster& mon, int pow)
243 {
244 const float x = mon.props[IOOD_X];
245 const float y = mon.props[IOOD_Y];
246 float vx = mon.props[IOOD_VX];
247 float vy = mon.props[IOOD_VY];
248
249 _normalize(vx, vy);
250
251 if (pow < 10)
252 pow = 10;
253 const float off = random_choose(-0.25, 0.25);
254 float tan = (random2(31) - 15) * 0.019; // approx from degrees
255 tan *= 75.0 / pow;
256 const int inaccuracy = caster ? caster->inaccuracy() : 0;
257 if (inaccuracy > 0)
258 tan *= 2 * inaccuracy;
259
260 // Cast either from left or right hand.
261 mon.props[IOOD_X] = x + vy*off;
262 mon.props[IOOD_Y] = y - vx*off;
263 // And off the direction a bit.
264 mon.props[IOOD_VX] = vx + vy*tan;
265 mon.props[IOOD_VY] = vy - vx*tan;
266 }
267
268 // Alas, too much differs to reuse beam shield blocks :(
_iood_shielded(monster & mon,actor & victim)269 static bool _iood_shielded(monster& mon, actor &victim)
270 {
271 if (!victim.shielded() || victim.incapacitated())
272 return false;
273
274 const int to_hit = 15 + (mons_is_projectile(mon.type) ?
275 mon.props[IOOD_POW].get_short()/12 : mon.get_hit_dice()/2);
276 const int con_block = random2(to_hit + victim.shield_block_penalty());
277 const int pro_block = victim.shield_bonus();
278 dprf("iood shield: pro %d, con %d", pro_block, con_block);
279 return pro_block >= con_block;
280 }
281
iood_damage(int pow,int dist)282 dice_def iood_damage(int pow, int dist)
283 {
284 pow = stepdown_value(pow, 30, 30, 200, -1);
285 if (dist < 4)
286 pow = pow * (dist*2+3) / 10;
287 return dice_def(9, pow / 4);
288 }
289
_iood_hit(monster & mon,const coord_def & pos,bool big_boom=false)290 static bool _iood_hit(monster& mon, const coord_def &pos, bool big_boom = false)
291 {
292 bolt beam;
293 beam.name = "orb of destruction";
294 beam.flavour = BEAM_DEVASTATION;
295 beam.attitude = mon.attitude;
296
297 actor *caster = actor_by_mid(mon.summoner);
298 if (!caster) // caster is dead/gone, blame the orb itself (as its
299 caster = &mon; // friendliness is correct)
300 beam.set_agent(caster);
301 if (mon.props.exists(IOOD_REFLECTOR))
302 {
303 beam.reflections = 1;
304
305 const mid_t refl_mid = mon.props[IOOD_REFLECTOR].get_int64();
306
307 if (refl_mid == MID_PLAYER)
308 beam.reflector = MID_PLAYER;
309 else
310 {
311 // If the reflecting monster has died, credit the original caster.
312 const monster * const rmon = monster_by_mid(refl_mid);
313 beam.reflector = rmon ? refl_mid : caster->mid;
314 }
315 }
316 beam.colour = WHITE;
317 beam.glyph = dchar_glyph(DCHAR_FIRED_BURST);
318 beam.range = 1;
319 beam.source = pos;
320 beam.target = pos;
321 beam.hit = AUTOMATIC_HIT;
322 beam.source_name = mon.props[IOOD_CASTER].get_string();
323 beam.origin_spell = SPELL_IOOD;
324
325 const int pow = mon.props[IOOD_POW].get_short();
326 const int dist = mon.props[IOOD_DIST].get_int();
327 ASSERT(dist >= 0);
328 beam.damage = iood_damage(pow, dist);
329
330 if (dist < 3)
331 beam.name = "wavering " + beam.name;
332 if (dist < 2)
333 beam.hit_verb = "weakly hits";
334 beam.ex_size = 1;
335 beam.loudness = 7;
336
337 monster_die(mon, KILL_DISMISSED, NON_MONSTER);
338
339 if (big_boom)
340 beam.explode(true, false);
341 else
342 beam.fire();
343
344 return true;
345 }
346
347 // returns true if the orb is gone
iood_act(monster & mon,bool no_trail)348 bool iood_act(monster& mon, bool no_trail)
349 {
350 ASSERT(mons_is_projectile(mon.type));
351
352 float x = mon.props[IOOD_X];
353 float y = mon.props[IOOD_Y];
354 float vx = mon.props[IOOD_VX];
355 float vy = mon.props[IOOD_VY];
356
357 dprf("iood_act: pos=(%d,%d) rpos=(%f,%f) v=(%f,%f) foe=%d",
358 mon.pos().x, mon.pos().y,
359 x, y, vx, vy, mon.foe);
360
361 if (!vx && !vy) // not initialized
362 {
363 _iood_stop(mon);
364 return true;
365 }
366
367 _normalize(vx, vy);
368
369 const actor *foe = mon.get_foe();
370 // If the target is gone, the orb continues on a ballistic course since
371 // picking a new one would require intelligence.
372
373 // IOODs can't home in on a submerged creature.
374 if (foe && !foe->submerged())
375 {
376 const coord_def target = foe->pos();
377 float dx = target.x - x;
378 float dy = target.y - y;
379 _normalize(dx, dy);
380
381 // Special case:
382 // Moving diagonally when the orb is just about to hit you
383 // 2
384 // ->*1
385 // (from 1 to 2) would be a guaranteed escape. This may be
386 // realistic (strafing!), but since the game has no non-cheesy
387 // means of waiting a small fraction of a turn, we don't want it.
388 const int old_t_pos = mon.props[IOOD_TPOS].get_short();
389 const coord_def rpos(static_cast<int>(round(x)), static_cast<int>(round(y)));
390 if (old_t_pos && old_t_pos != (256 * target.x + target.y)
391 && (rpos - target).rdist() <= 1
392 // ... but following an orb is ok.
393 && _in_front(vx, vy, dx, dy, 1.5)) // ~97 degrees
394 {
395 vx = dx;
396 vy = dy;
397 }
398 mon.props[IOOD_TPOS].get_short() = 256 * target.x + target.y;
399
400 if (!_in_front(vx, vy, dx, dy, 0.3)) // ~17 degrees
401 {
402 float ax, ay;
403 if (dy*vx < dx*vy)
404 ax = vy, ay = -vx, dprf("iood: veering left");
405 else
406 ax = -vy, ay = vx, dprf("iood: veering right");
407 vx += ax * 0.3;
408 vy += ay * 0.3;
409 }
410 else
411 dprf("iood: keeping course");
412
413 _normalize(vx, vy);
414 mon.props[IOOD_VX] = vx;
415 mon.props[IOOD_VY] = vy;
416 }
417
418 move_again:
419 coord_def starting_pos = (mon.pos() == coord_def()) ?
420 coord_def(x, y) : mon.pos();
421
422 x += vx;
423 y += vy;
424
425 mon.props[IOOD_X] = x;
426 mon.props[IOOD_Y] = y;
427 mon.props[IOOD_DIST].get_int()++;
428
429 const coord_def pos(static_cast<int>(round(x)), static_cast<int>(round(y)));
430 if (!in_bounds(pos))
431 {
432 _iood_stop(mon);
433 return true;
434 }
435
436 if (mon.props.exists(IOOD_FLAWED))
437 {
438 const actor *caster = actor_by_mid(mon.summoner);
439 if (!caster || caster->pos().origin() ||
440 (caster->pos() - pos).rdist() > LOS_RADIUS)
441 { // not actual vision, because of the smoke trail
442 _iood_stop(mon);
443 return true;
444 }
445 }
446
447 if (pos == mon.pos())
448 return false;
449
450 if (!no_trail)
451 place_cloud(CLOUD_MAGIC_TRAIL, starting_pos, 2 + random2(3), &mon);
452
453 actor *victim = actor_at(pos);
454 if (cell_is_solid(pos) || victim)
455 {
456 if (cell_is_solid(pos)
457 && you.see_cell(pos)
458 && you.see_cell(starting_pos))
459 {
460 mprf("%s hits %s.", mon.name(DESC_THE, true).c_str(),
461 feature_description_at(pos, false, DESC_A).c_str());
462 }
463
464 monster* mons = (victim && victim->is_monster()) ?
465 (monster*) victim : 0;
466
467 if (mons && mons_is_projectile(victim->type))
468 {
469 // Weak orbs just fizzle instead of exploding.
470 if (mons->props[IOOD_DIST].get_int() < 2
471 || mon.props[IOOD_DIST].get_int() < 2)
472 {
473 if (mons->props[IOOD_DIST].get_int() < 2)
474 {
475 if (you.see_cell(pos))
476 mpr("The orb fizzles.");
477 monster_die(*mons, KILL_DISMISSED, NON_MONSTER);
478 }
479
480 // Return, if the acting orb fizzled.
481 if (mon.props[IOOD_DIST].get_int() < 2)
482 {
483 if (you.see_cell(pos))
484 mpr("The orb fizzles.");
485 monster_die(mon, KILL_DISMISSED, NON_MONSTER);
486 return true;
487 }
488 }
489 else
490 {
491 if (mon.observable())
492 mpr("The orbs collide in a blinding explosion!");
493 else
494 mpr("You hear a loud magical explosion!");
495 noisy(40, pos);
496 monster_die(*mons, KILL_DISMISSED, NON_MONSTER);
497 _iood_hit(mon, pos, true);
498 return true;
499 }
500 }
501
502 if (mons && (mons->submerged() || mons->type == MONS_BATTLESPHERE))
503 {
504 // Try to swap with the submerged creature.
505 if (mon.swap_with(mons))
506 {
507 dprf("iood: Swapping with a submerged monster.");
508 return false;
509 }
510 else // if swap fails, move ahead
511 {
512 dprf("iood: Boosting above a submerged monster (can't swap).");
513 mon.lose_energy(EUT_MOVE);
514 goto move_again;
515 }
516 }
517
518 if (victim && _iood_shielded(mon, *victim))
519 {
520 item_def *shield = victim->shield();
521 if ((!shield || !shield_reflects(*shield)) && !victim->reflection())
522 {
523 if (victim->is_player())
524 mprf("You block %s.", mon.name(DESC_THE, true).c_str());
525 else
526 {
527 simple_monster_message(*mons, (" blocks "
528 + mon.name(DESC_THE, true) + ".").c_str());
529 }
530 victim->shield_block_succeeded();
531 _iood_stop(mon);
532 return true;
533 }
534
535 if (victim->is_player())
536 {
537 if (shield && shield_reflects(*shield))
538 {
539 mprf("Your %s reflects %s!",
540 shield->name(DESC_PLAIN).c_str(),
541 mon.name(DESC_THE, true).c_str());
542 ident_reflector(shield);
543 }
544 else // has reflection property not from shield
545 {
546 mprf("%s reflects off an invisible shield around you!",
547 mon.name(DESC_THE, true).c_str());
548 }
549 }
550 else if (you.see_cell(pos))
551 {
552 if (victim->observable())
553 {
554 if (shield && shield_reflects(*shield))
555 {
556 mprf("%s reflects %s off %s %s!",
557 victim->name(DESC_THE, true).c_str(),
558 mon.name(DESC_THE, true).c_str(),
559 victim->pronoun(PRONOUN_POSSESSIVE).c_str(),
560 shield->name(DESC_PLAIN).c_str());
561 ident_reflector(shield);
562 }
563 else
564 {
565 mprf("%s reflects off an invisible shield around %s!",
566 mon.name(DESC_THE, true).c_str(),
567 victim->name(DESC_THE, true).c_str());
568
569 item_def *amulet = victim->slot_item(EQ_AMULET);
570 if (amulet)
571 ident_reflector(amulet);
572 }
573 }
574 else
575 {
576 mprf("%s bounces off of thin air!",
577 mon.name(DESC_THE, true).c_str());
578 }
579 }
580 victim->shield_block_succeeded();
581
582 // mid_t is unsigned so won't fit in a plain int
583 mon.props[IOOD_REFLECTOR] = (int64_t) victim->mid;
584 mon.props[IOOD_VX] = vx = -vx;
585 mon.props[IOOD_VY] = vy = -vy;
586
587 // Need to get out of the victim's square.
588
589 // If you're next to the caster and both of you wear shields of
590 // reflection, this can lead to a brief game of ping-pong, but
591 // rapidly increasing shield penalties will make it short.
592 mon.lose_energy(EUT_MOVE);
593 goto move_again;
594 }
595
596 if (_iood_hit(mon, pos))
597 return true;
598 }
599
600 if (!mon.move_to_pos(pos))
601 {
602 _iood_stop(mon);
603 return true;
604 }
605
606 // move_to_pos() just trashed the coords, set them again
607 mon.props[IOOD_X] = x;
608 mon.props[IOOD_Y] = y;
609
610 return false;
611 }
612
613 // Reduced copy of iood_act to move the orb while the player is off-level.
614 // Just goes straight and dissipates instead of hitting anything.
_iood_catchup_move(monster & mon)615 static bool _iood_catchup_move(monster& mon)
616 {
617 float x = mon.props[IOOD_X];
618 float y = mon.props[IOOD_Y];
619 float vx = mon.props[IOOD_VX];
620 float vy = mon.props[IOOD_VY];
621
622 if (!vx && !vy) // not initialized
623 {
624 _iood_stop(mon, false);
625 return true;
626 }
627
628 _normalize(vx, vy);
629
630 x += vx;
631 y += vy;
632
633 mon.props[IOOD_X] = x;
634 mon.props[IOOD_Y] = y;
635 mon.props[IOOD_DIST].get_int()++;
636
637 const coord_def pos(static_cast<int>(round(x)), static_cast<int>(round(y)));
638 if (!in_bounds(pos))
639 {
640 _iood_stop(mon, true);
641 return true;
642 }
643
644 if (pos == mon.pos())
645 return false;
646
647 actor *victim = actor_at(pos);
648 if (cell_is_solid(pos) || victim)
649 {
650 // Just dissipate instead of hitting something.
651 _iood_stop(mon, true);
652 return true;
653 }
654
655 if (!mon.move_to_pos(pos))
656 {
657 _iood_stop(mon);
658 return true;
659 }
660
661 // move_to_pos() just trashed the coords, set them again
662 mon.props[IOOD_X] = x;
663 mon.props[IOOD_Y] = y;
664
665 return false;
666 }
667
iood_catchup(monster * mons,int pturns)668 void iood_catchup(monster* mons, int pturns)
669 {
670 monster& mon = *mons;
671 ASSERT(mons_is_projectile(*mons));
672
673 const int moves = pturns * mon.speed / BASELINE_DELAY;
674
675 // Handle some cases for IOOD only
676 if (mons_is_projectile(*mons))
677 {
678 if (moves > 50)
679 {
680 _iood_stop(mon, false);
681 return;
682 }
683
684 if (mon.props[IOOD_KC].get_byte() == KC_YOU)
685 {
686 // Left player's vision.
687 _iood_stop(mon, false);
688 return;
689 }
690 }
691
692 for (int i = 0; i < moves; ++i)
693 if (_iood_catchup_move(mon))
694 return;
695 }
696