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