added support for levels larger than visible playfield in MM engine
[rocksndiamonds.git] / src / game_mm / mm_game.c
1 // ============================================================================
2 // Mirror Magic -- McDuffin's Revenge
3 // ----------------------------------------------------------------------------
4 // (c) 1994-2017 by Artsoft Entertainment
5 //                  Holger Schemel
6 //                  info@artsoft.org
7 //                  https://www.artsoft.org/
8 // ----------------------------------------------------------------------------
9 // mm_game.c
10 // ============================================================================
11
12 #include <math.h>
13
14 #include "main_mm.h"
15
16 #include "mm_main.h"
17 #include "mm_game.h"
18 #include "mm_tools.h"
19
20 // graphic position values for game controls
21 #define ENERGY_XSIZE            32
22 #define ENERGY_YSIZE            MAX_LASER_ENERGY
23 #define OVERLOAD_XSIZE          ENERGY_XSIZE
24 #define OVERLOAD_YSIZE          MAX_LASER_OVERLOAD
25
26 // values for Explode_MM()
27 #define EX_PHASE_START          0
28 #define EX_TYPE_NONE            0
29 #define EX_TYPE_NORMAL          (1 << 0)
30
31 // special positions in the game control window (relative to control window)
32 #define XX_LEVEL                36
33 #define YY_LEVEL                23
34 #define XX_KETTLES              29
35 #define YY_KETTLES              63
36 #define XX_SCORE                22
37 #define YY_SCORE                101
38 #define XX_ENERGY               8
39 #define YY_ENERGY               158
40 #define XX_OVERLOAD             60
41 #define YY_OVERLOAD             YY_ENERGY
42
43 // special positions in the game control window (relative to main window)
44 #define DX_LEVEL                (DX + XX_LEVEL)
45 #define DY_LEVEL                (DY + YY_LEVEL)
46 #define DX_KETTLES              (DX + XX_KETTLES)
47 #define DY_KETTLES              (DY + YY_KETTLES)
48 #define DX_SCORE                (DX + XX_SCORE)
49 #define DY_SCORE                (DY + YY_SCORE)
50 #define DX_ENERGY               (DX + XX_ENERGY)
51 #define DY_ENERGY               (DY + YY_ENERGY)
52 #define DX_OVERLOAD             (DX + XX_OVERLOAD)
53 #define DY_OVERLOAD             (DY + YY_OVERLOAD)
54
55 #define IS_LOOP_SOUND(s)        ((s) == SND_FUEL)
56 #define IS_MUSIC_SOUND(s)       ((s) == SND_TYGER || (s) == SND_VOYAGER)
57
58 // game button identifiers
59 #define GAME_CTRL_ID_LEFT       0
60 #define GAME_CTRL_ID_MIDDLE     1
61 #define GAME_CTRL_ID_RIGHT      2
62
63 #define NUM_GAME_BUTTONS        3
64
65 // values for DrawLaser()
66 #define DL_LASER_DISABLED       0
67 #define DL_LASER_ENABLED        1
68
69 // values for 'click_delay_value' in ClickElement()
70 #define CLICK_DELAY_FIRST       12      // delay (frames) after first click
71 #define CLICK_DELAY             6       // delay (frames) for pressed butten
72
73 #define AUTO_ROTATE_DELAY       CLICK_DELAY
74 #define INIT_GAME_ACTIONS_DELAY (ONE_SECOND_DELAY / GAME_FRAME_DELAY)
75 #define NUM_INIT_CYCLE_STEPS    16
76 #define PACMAN_MOVE_DELAY       12
77 #define ENERGY_DELAY            (ONE_SECOND_DELAY / GAME_FRAME_DELAY)
78 #define HEALTH_DEC_DELAY        3
79 #define HEALTH_INC_DELAY        9
80 #define HEALTH_DELAY(x)         ((x) ? HEALTH_DEC_DELAY : HEALTH_INC_DELAY)
81
82 #define BEGIN_NO_HEADLESS                       \
83   {                                             \
84     boolean last_headless = program.headless;   \
85                                                 \
86     program.headless = FALSE;                   \
87
88 #define END_NO_HEADLESS                         \
89     program.headless = last_headless;           \
90   }                                             \
91
92 // forward declaration for internal use
93 static int MovingOrBlocked2Element_MM(int, int);
94 static void Bang_MM(int, int);
95 static void RaiseScore_MM(int);
96 static void RaiseScoreElement_MM(int);
97 static void RemoveMovingField_MM(int, int);
98 static void InitMovingField_MM(int, int, int);
99 static void ContinueMoving_MM(int, int);
100
101 static void AddLaserEdge(int, int);
102 static void ScanLaser(void);
103 static void DrawLaser(int, int);
104 static boolean HitElement(int, int);
105 static boolean HitOnlyAnEdge(int);
106 static boolean HitPolarizer(int, int);
107 static boolean HitBlock(int, int);
108 static boolean HitLaserSource(int, int);
109 static boolean HitLaserDestination(int, int);
110 static boolean HitReflectingWalls(int, int);
111 static boolean HitAbsorbingWalls(int, int);
112 static void RotateMirror(int, int, int);
113 static boolean ObjHit(int, int, int);
114 static void DeletePacMan(int, int);
115 static void MovePacMen(void);
116
117 // bitmap for laser beam detection
118 static Bitmap *laser_bitmap = NULL;
119
120 // variables for laser control
121 static int last_LX = 0, last_LY = 0, last_hit_mask = 0;
122 static int hold_x = -1, hold_y = -1;
123
124 // variables for pacman control
125 static int pacman_nr = -1;
126
127 // various game engine delay counters
128 static DelayCounter rotate_delay = { AUTO_ROTATE_DELAY };
129 static DelayCounter pacman_delay = { PACMAN_MOVE_DELAY };
130 static DelayCounter energy_delay = { ENERGY_DELAY };
131 static DelayCounter overload_delay = { 0 };
132
133 // element mask positions for scanning pixels of MM elements
134 #define MM_MASK_MCDUFFIN_RIGHT  0
135 #define MM_MASK_MCDUFFIN_UP     1
136 #define MM_MASK_MCDUFFIN_LEFT   2
137 #define MM_MASK_MCDUFFIN_DOWN   3
138 #define MM_MASK_GRID_1          4
139 #define MM_MASK_GRID_2          5
140 #define MM_MASK_GRID_3          6
141 #define MM_MASK_GRID_4          7
142 #define MM_MASK_RECTANGLE       8
143 #define MM_MASK_CIRCLE          9
144
145 #define NUM_MM_MASKS            10
146
147 // element masks for scanning pixels of MM elements
148 static const char mm_masks[NUM_MM_MASKS][16][16 + 1] =
149 {
150   {
151     "                ",
152     "    XXXXX       ",
153     "   XXXXXXX      ",
154     "  XXXXXXXXXXX   ",
155     "  XXXXXXXXXXXXX ",
156     "  XXXXXXXXXXXXXX",
157     "  XXXXXXXXXXXXXX",
158     "  XXXXXXXXXXXXX ",
159     "  XXXXXXXXXXXXX ",
160     "  XXXXXXXXXXXXX ",
161     "  XXXXXXXXXXXXX ",
162     "  XXXXXXXXXXXXX ",
163     "  XXXXXXXXXXXXX ",
164     "  XXXXXXXXXXXXX ",
165     "  XXXXXXXXXXXX  ",
166     "  XXXXXXXXXXXX  ",
167   },
168   {
169     "                ",
170     "    XXXXXXXX    ",
171     "  XXXXXXXXXXXX  ",
172     " XXXXXXXXXXXXXX ",
173     " XXXXXXXXXXXXXX ",
174     " XXXXXXXXXXXXXX ",
175     " XXXXXXXXXXXXXX ",
176     " XXXXXXXXXXXXXX ",
177     " XXXXXXXXXXXXXX ",
178     " XXXXXXXXXXXXXX ",
179     " XXXXXXXXXXXXXX ",
180     " XXXXXXXXXXXXXX ",
181     " XXXXXXXXXXXXXX ",
182     "  XXXXXXXXXXXXX ",
183     "  XXXXXXXXXXXX  ",
184     "  XXXXXXXXXXXX  ",
185   },
186   {
187     "                ",
188     "    XXXXXX      ",
189     "  XXXXXXXXX     ",
190     " XXXXXXXXXXX    ",
191     "XXXXXXXXXXXXX   ",
192     "XXXXXXXXXXXXX   ",
193     "XXXXXXXXXXXXXX  ",
194     " XXXXXXXXXXXXX  ",
195     " XXXXXXXXXXXXX  ",
196     " XXXXXXXXXXXXX  ",
197     " XXXXXXXXXXXXX  ",
198     " XXXXXXXXXXXXX  ",
199     " XXXXXXXXXXXXX  ",
200     " XXXXXXXXXXXXX  ",
201     "  XXXXXXXXXXXX  ",
202     "  XXXXXXXXXXXX  ",
203   },
204   {
205     "                ",
206     "    XXXXXX      ",
207     "   XXXXXXXX     ",
208     "  XXXXXXXXXX    ",
209     "  XXXXXXXXXXX   ",
210     "  XXXXXXXXXXX   ",
211     "  XXXXXXXXXXXX  ",
212     "  XXXXXXXXXXXX  ",
213     "  XXXXXXXXXXXX  ",
214     "  XXXXXXXXXXXX  ",
215     "  XXXXXXXXXXXX  ",
216     "  XXXXXXXXXXXX  ",
217     "  XXXXXXXXXXXX  ",
218     "  XXXXXXXXXXXX  ",
219     "  XXXXXXXXXXXX  ",
220     "  XXXXX  XXXXX  ",
221   },
222   {
223     " XXXXXX  XXXXXX ",
224     "XXXXXXXXXXXXXXXX",
225     "XXXXXXXXXXXXXXXX",
226     "XXXXXXXXXXXXXXXX",
227     "XXXXXXXXXXXXXXXX",
228     "XXXXXXXXXXXXXXXX",
229     "XXXXXXXXXXXXXXXX",
230     "                ",
231     "                ",
232     "XXXXXXXXXXXXXXXX",
233     "XXXXXXXXXXXXXXXX",
234     "XXXXXXXXXXXXXXXX",
235     "XXXXXXXXXXXXXXXX",
236     "XXXXXXXXXXXXXXXX",
237     "XXXXXXXXXXXXXXXX",
238     " XXXXXX  XXXXXX ",
239   },
240   {
241     " XXXXXX  XXXXXX ",
242     "XXXXXXX  XXXXXXX",
243     "XXXXXXX  XXXXXXX",
244     "XXXXXXX  XXXXXXX",
245     "XXXXXXX  XXXXXXX",
246     "XXXXXXX  XXXXXXX",
247     "XXXXXXX  XXXXXXX",
248     " XXXXXX  XXXXXX ",
249     " XXXXXX  XXXXXX ",
250     "XXXXXXX  XXXXXXX",
251     "XXXXXXX  XXXXXXX",
252     "XXXXXXX  XXXXXXX",
253     "XXXXXXX  XXXXXXX",
254     "XXXXXXX  XXXXXXX",
255     "XXXXXXX  XXXXXXX",
256     " XXXXXX  XXXXXX ",
257   },
258   {
259     "     XX  XXXXX  ",
260     "    XXX  XXXX   ",
261     "   XXXX  XXX   X",
262     "  XXXXXXXXX   XX",
263     " XXXXXXXXX   XXX",
264     "XXXXXXXXX   XXXX",
265     "XXXXXXXX   XXXXX",
266     "   XXXX   XXX   ",
267     "   XXX   XXXX   ",
268     "XXXXX   XXXXXXXX",
269     "XXXX   XXXXXXXXX",
270     "XXX   XXXXXXXXX ",
271     "XX   XXXXXXXXX  ",
272     "X   XXX  XXXX   ",
273     "   XXXX  XXX    ",
274     "  XXXXX  XX     ",
275   },
276   {
277     "  XXXXX  XX     ",
278     "   XXXX  XXX    ",
279     "X   XXX  XXXX   ",
280     "XX   XXXXXXXXX  ",
281     "XXX   XXXXXXXXX ",
282     "XXXX   XXXXXXXXX",
283     "XXXXX   XXXXXXXX",
284     "   XXX   XXXX   ",
285     "   XXXX   XXX   ",
286     "XXXXXXXX   XXXXX",
287     "XXXXXXXXX   XXXX",
288     " XXXXXXXXX   XXX",
289     "  XXXXXXXXX   XX",
290     "   XXXX  XXX   X",
291     "    XXX  XXXX   ",
292     "     XX  XXXXX  ",
293   },
294   {
295     "XXXXXXXXXXXXXXXX",
296     "XXXXXXXXXXXXXXXX",
297     "XXXXXXXXXXXXXXXX",
298     "XXXXXXXXXXXXXXXX",
299     "XXXXXXXXXXXXXXXX",
300     "XXXXXXXXXXXXXXXX",
301     "XXXXXXXXXXXXXXXX",
302     "XXXXXXXXXXXXXXXX",
303     "XXXXXXXXXXXXXXXX",
304     "XXXXXXXXXXXXXXXX",
305     "XXXXXXXXXXXXXXXX",
306     "XXXXXXXXXXXXXXXX",
307     "XXXXXXXXXXXXXXXX",
308     "XXXXXXXXXXXXXXXX",
309     "XXXXXXXXXXXXXXXX",
310     "XXXXXXXXXXXXXXXX",
311   },
312   {
313     "                ",
314     "      XXXX      ",
315     "    XXXXXXXX    ",
316     "   XXXXXXXXXX   ",
317     "  XXXXXXXXXXXX  ",
318     "  XXXXXXXXXXXX  ",
319     " XXXXXXXXXXXXXX ",
320     " XXXXXXXXXXXXXX ",
321     " XXXXXXXXXXXXXX ",
322     " XXXXXXXXXXXXXX ",
323     "  XXXXXXXXXXXX  ",
324     "  XXXXXXXXXXXX  ",
325     "   XXXXXXXXXX   ",
326     "    XXXXXXXX    ",
327     "      XXXX      ",
328     "                ",
329   },
330 };
331
332 static int get_element_angle(int element)
333 {
334   int element_phase = get_element_phase(element);
335
336   if (IS_MIRROR_FIXED(element) ||
337       IS_MCDUFFIN(element) ||
338       IS_LASER(element) ||
339       IS_RECEIVER(element))
340     return 4 * element_phase;
341   else
342     return element_phase;
343 }
344
345 static int get_opposite_angle(int angle)
346 {
347   int opposite_angle = angle + ANG_RAY_180;
348
349   // make sure "opposite_angle" is in valid interval [0, 15]
350   return (opposite_angle + 16) % 16;
351 }
352
353 static int get_mirrored_angle(int laser_angle, int mirror_angle)
354 {
355   int reflected_angle = 16 - laser_angle + mirror_angle;
356
357   // make sure "reflected_angle" is in valid interval [0, 15]
358   return (reflected_angle + 16) % 16;
359 }
360
361 static void DrawLaserLines(struct XY *points, int num_points, int mode)
362 {
363   Pixel pixel_drawto = (mode == DL_LASER_ENABLED ? pen_ray     : pen_bg);
364   Pixel pixel_buffer = (mode == DL_LASER_ENABLED ? WHITE_PIXEL : BLACK_PIXEL);
365
366   DrawLines(drawto_mm, points, num_points, pixel_drawto);
367
368   BEGIN_NO_HEADLESS
369   {
370     DrawLines(laser_bitmap, points, num_points, pixel_buffer);
371   }
372   END_NO_HEADLESS
373 }
374
375 static boolean CheckLaserPixel(int x, int y)
376 {
377   Pixel pixel;
378
379   BEGIN_NO_HEADLESS
380   {
381     pixel = ReadPixel(laser_bitmap, x, y);
382   }
383   END_NO_HEADLESS
384
385   return (pixel == WHITE_PIXEL);
386 }
387
388 static void CheckExitMM(void)
389 {
390   int exit_element = EL_EMPTY;
391   int exit_x = 0;
392   int exit_y = 0;
393   int x, y;
394   static int xy[4][2] =
395   {
396     { +1,  0 },
397     {  0, -1 },
398     { -1,  0 },
399     {  0, +1 }
400   };
401
402   for (y = 0; y < lev_fieldy; y++)
403   {
404     for (x = 0; x < lev_fieldx; x++)
405     {
406       if (Tile[x][y] == EL_EXIT_CLOSED)
407       {
408         // initiate opening animation of exit door
409         Tile[x][y] = EL_EXIT_OPENING;
410
411         exit_element = EL_EXIT_OPEN;
412         exit_x = x;
413         exit_y = y;
414       }
415       else if (IS_RECEIVER(Tile[x][y]))
416       {
417         // remove field that blocks receiver
418         int phase = Tile[x][y] - EL_RECEIVER_START;
419         int blocking_x, blocking_y;
420
421         blocking_x = x + xy[phase][0];
422         blocking_y = y + xy[phase][1];
423
424         if (IN_LEV_FIELD(blocking_x, blocking_y))
425         {
426           Tile[blocking_x][blocking_y] = EL_EMPTY;
427
428           DrawField_MM(blocking_x, blocking_y);
429         }
430
431         exit_element = EL_RECEIVER;
432         exit_x = x;
433         exit_y = y;
434       }
435     }
436   }
437
438   if (exit_element != EL_EMPTY)
439     PlayLevelSound_MM(exit_x, exit_y, exit_element, MM_ACTION_OPENING);
440 }
441
442 static void SetLaserColor(int brightness)
443 {
444   int color_min = 0x00;
445   int color_max = brightness;           // (0x00 <= brightness <= 0xFF)
446   int color_up   = color_max * laser.overload_value / MAX_LASER_OVERLOAD;
447   int color_down = color_max - color_up;
448
449   pen_ray =
450     GetPixelFromRGB(window,
451                     (game_mm.laser_red   ? color_max  : color_up),
452                     (game_mm.laser_green ? color_down : color_min),
453                     (game_mm.laser_blue  ? color_down : color_min));
454 }
455
456 static void InitMovDir_MM(int x, int y)
457 {
458   int element = Tile[x][y];
459   static int direction[3][4] =
460   {
461     { MV_RIGHT, MV_UP,    MV_LEFT,  MV_DOWN },
462     { MV_LEFT,  MV_DOWN,  MV_RIGHT, MV_UP   },
463     { MV_LEFT,  MV_RIGHT, MV_UP,    MV_DOWN }
464   };
465
466   switch (element)
467   {
468     case EL_PACMAN_RIGHT:
469     case EL_PACMAN_UP:
470     case EL_PACMAN_LEFT:
471     case EL_PACMAN_DOWN:
472       Tile[x][y] = EL_PACMAN;
473       MovDir[x][y] = direction[0][element - EL_PACMAN_RIGHT];
474       break;
475
476     default:
477       break;
478   }
479 }
480
481 static void InitField(int x, int y, boolean init_game)
482 {
483   int element = Tile[x][y];
484
485   switch (element)
486   {
487     case EL_DF_EMPTY:
488       Tile[x][y] = EL_EMPTY;
489       break;
490
491     case EL_KETTLE:
492     case EL_CELL:
493       if (init_game && native_mm_level.auto_count_kettles)
494         game_mm.kettles_still_needed++;
495       break;
496
497     case EL_LIGHTBULB_OFF:
498       game_mm.lights_still_needed++;
499       break;
500
501     default:
502       if (IS_MIRROR(element) ||
503           IS_BEAMER_OLD(element) ||
504           IS_BEAMER(element) ||
505           IS_POLAR(element) ||
506           IS_POLAR_CROSS(element) ||
507           IS_DF_MIRROR(element) ||
508           IS_DF_MIRROR_AUTO(element) ||
509           IS_GRID_STEEL_AUTO(element) ||
510           IS_GRID_WOOD_AUTO(element) ||
511           IS_FIBRE_OPTIC(element))
512       {
513         if (IS_BEAMER_OLD(element))
514         {
515           Tile[x][y] = EL_BEAMER_BLUE_START + (element - EL_BEAMER_START);
516           element = Tile[x][y];
517         }
518
519         if (!IS_FIBRE_OPTIC(element))
520         {
521           static int steps_grid_auto = 0;
522
523           if (game_mm.num_cycle == 0)   // initialize cycle steps for grids
524             steps_grid_auto = RND(16) * (RND(2) ? -1 : +1);
525
526           if (IS_GRID_STEEL_AUTO(element) ||
527               IS_GRID_WOOD_AUTO(element))
528             game_mm.cycle[game_mm.num_cycle].steps = steps_grid_auto;
529           else
530             game_mm.cycle[game_mm.num_cycle].steps = RND(16) * (RND(2) ? -1 : +1);
531
532           game_mm.cycle[game_mm.num_cycle].x = x;
533           game_mm.cycle[game_mm.num_cycle].y = y;
534           game_mm.num_cycle++;
535         }
536
537         if (IS_BEAMER(element) || IS_FIBRE_OPTIC(element))
538         {
539           int beamer_nr = BEAMER_NR(element);
540           int nr = laser.beamer[beamer_nr][0].num;
541
542           laser.beamer[beamer_nr][nr].x = x;
543           laser.beamer[beamer_nr][nr].y = y;
544           laser.beamer[beamer_nr][nr].num = 1;
545         }
546       }
547       else if (IS_PACMAN(element))
548       {
549         InitMovDir_MM(x, y);
550       }
551       else if (IS_MCDUFFIN(element) || IS_LASER(element))
552       {
553         if (init_game)
554         {
555           laser.start_edge.x = x;
556           laser.start_edge.y = y;
557           laser.start_angle = get_element_angle(element);
558         }
559
560         if (IS_MCDUFFIN(element))
561         {
562           game_mm.laser_red   = native_mm_level.mm_laser_red;
563           game_mm.laser_green = native_mm_level.mm_laser_green;
564           game_mm.laser_blue  = native_mm_level.mm_laser_blue;
565         }
566         else
567         {
568           game_mm.laser_red   = native_mm_level.df_laser_red;
569           game_mm.laser_green = native_mm_level.df_laser_green;
570           game_mm.laser_blue  = native_mm_level.df_laser_blue;
571         }
572
573         game_mm.has_mcduffin = (IS_MCDUFFIN(element));
574       }
575
576       break;
577   }
578 }
579
580 static void InitCycleElements_RotateSingleStep(void)
581 {
582   int i;
583
584   if (game_mm.num_cycle == 0)   // no elements to cycle
585     return;
586
587   for (i = 0; i < game_mm.num_cycle; i++)
588   {
589     int x = game_mm.cycle[i].x;
590     int y = game_mm.cycle[i].y;
591     int step = SIGN(game_mm.cycle[i].steps);
592     int last_element = Tile[x][y];
593     int next_element = get_rotated_element(last_element, step);
594
595     if (!game_mm.cycle[i].steps)
596       continue;
597
598     Tile[x][y] = next_element;
599
600     game_mm.cycle[i].steps -= step;
601   }
602 }
603
604 static void InitLaser(void)
605 {
606   int start_element = Tile[laser.start_edge.x][laser.start_edge.y];
607   int step = (IS_LASER(start_element) ? 4 : 0);
608
609   LX = laser.start_edge.x * TILEX;
610   if (laser.start_angle == ANG_RAY_UP || laser.start_angle == ANG_RAY_DOWN)
611     LX += 14;
612   else
613     LX += (laser.start_angle == ANG_RAY_RIGHT ? 28 + step : 0 - step);
614
615   LY = laser.start_edge.y * TILEY;
616   if (laser.start_angle == ANG_RAY_UP || laser.start_angle == ANG_RAY_DOWN)
617     LY += (laser.start_angle == ANG_RAY_DOWN ? 28 + step : 0 - step);
618   else
619     LY += 14;
620
621   XS = 2 * Step[laser.start_angle].x;
622   YS = 2 * Step[laser.start_angle].y;
623
624   laser.current_angle = laser.start_angle;
625
626   laser.num_damages = 0;
627   laser.num_edges = 0;
628   laser.num_beamers = 0;
629   laser.beamer_edge[0] = 0;
630
631   laser.dest_element = EL_EMPTY;
632   laser.wall_mask = 0;
633
634   AddLaserEdge(LX, LY);         // set laser starting edge
635
636   SetLaserColor(0xFF);
637 }
638
639 void InitGameEngine_MM(void)
640 {
641   int i, x, y;
642
643   BEGIN_NO_HEADLESS
644   {
645     // initialize laser bitmap to current playfield (screen) size
646     ReCreateBitmap(&laser_bitmap, drawto_mm->width, drawto_mm->height);
647     ClearRectangle(laser_bitmap, 0, 0, drawto_mm->width, drawto_mm->height);
648   }
649   END_NO_HEADLESS
650
651   // set global game control values
652   game_mm.num_cycle = 0;
653   game_mm.num_pacman = 0;
654
655   game_mm.score = 0;
656   game_mm.energy_left = 0;      // later set to "native_mm_level.time"
657   game_mm.kettles_still_needed =
658     (native_mm_level.auto_count_kettles ? 0 : native_mm_level.kettles_needed);
659   game_mm.lights_still_needed = 0;
660   game_mm.num_keys = 0;
661   game_mm.ball_choice_pos = 0;
662
663   game_mm.laser_red = FALSE;
664   game_mm.laser_green = FALSE;
665   game_mm.laser_blue = TRUE;
666   game_mm.has_mcduffin = TRUE;
667
668   game_mm.level_solved = FALSE;
669   game_mm.game_over = FALSE;
670   game_mm.game_over_cause = 0;
671   game_mm.game_over_message = NULL;
672
673   game_mm.laser_overload_value = 0;
674   game_mm.laser_enabled = FALSE;
675
676   // set global laser control values (must be set before "InitLaser()")
677   laser.start_edge.x = 0;
678   laser.start_edge.y = 0;
679   laser.start_angle = 0;
680
681   for (i = 0; i < MAX_NUM_BEAMERS; i++)
682     laser.beamer[i][0].num = laser.beamer[i][1].num = 0;
683
684   laser.overloaded = FALSE;
685   laser.overload_value = 0;
686   laser.fuse_off = FALSE;
687   laser.fuse_x = laser.fuse_y = -1;
688
689   laser.dest_element = EL_EMPTY;
690   laser.dest_element_last = EL_EMPTY;
691   laser.dest_element_last_x = -1;
692   laser.dest_element_last_y = -1;
693   laser.wall_mask = 0;
694
695   last_LX = 0;
696   last_LY = 0;
697   last_hit_mask = 0;
698
699   hold_x = -1;
700   hold_y = -1;
701
702   pacman_nr = -1;
703
704   CT = Ct = 0;
705
706   rotate_delay.count = 0;
707   pacman_delay.count = 0;
708   energy_delay.count = 0;
709   overload_delay.count = 0;
710
711   ClickElement(-1, -1, -1);
712
713   for (x = 0; x < lev_fieldx; x++)
714   {
715     for (y = 0; y < lev_fieldy; y++)
716     {
717       Tile[x][y] = Ur[x][y];
718       Hit[x][y] = Box[x][y] = 0;
719       Angle[x][y] = 0;
720       MovPos[x][y] = MovDir[x][y] = MovDelay[x][y] = 0;
721       Store[x][y] = Store2[x][y] = 0;
722       Stop[x][y] = FALSE;
723
724       InitField(x, y, TRUE);
725     }
726   }
727
728   DrawLevel_MM();
729 }
730
731 void InitGameActions_MM(void)
732 {
733   int num_init_game_frames = INIT_GAME_ACTIONS_DELAY;
734   int cycle_steps_done = 0;
735   int i;
736
737   InitLaser();
738
739   for (i = 0; i <= num_init_game_frames; i++)
740   {
741     if (i == num_init_game_frames)
742       StopSound_MM(SND_MM_GAME_LEVELTIME_CHARGING);
743     else if (setup.sound_loops)
744       PlaySoundLoop_MM(SND_MM_GAME_LEVELTIME_CHARGING);
745     else
746       PlaySound_MM(SND_MM_GAME_LEVELTIME_CHARGING);
747
748     game_mm.energy_left = native_mm_level.time * i / num_init_game_frames;
749
750     UpdateAndDisplayGameControlValues();
751
752     while (cycle_steps_done < NUM_INIT_CYCLE_STEPS * i / num_init_game_frames)
753     {
754       InitCycleElements_RotateSingleStep();
755
756       cycle_steps_done++;
757     }
758
759     AdvanceFrameCounter();
760     AdvanceGfxFrame();
761
762     DrawLevel_MM();
763
764     BackToFront_MM();
765
766 #ifdef DEBUG
767     if (setup.quick_doors)
768       continue;
769 #endif
770   }
771
772   ScanLaser();
773
774   if (game_mm.kettles_still_needed == 0)
775     CheckExitMM();
776
777   SetTileCursorXY(laser.start_edge.x, laser.start_edge.y);
778   SetTileCursorActive(TRUE);
779
780   // restart all delay counters after initially cycling game elements
781   ResetFrameCounter(&rotate_delay);
782   ResetFrameCounter(&pacman_delay);
783   ResetFrameCounter(&energy_delay);
784   ResetFrameCounter(&overload_delay);
785 }
786
787 static void FadeOutLaser(void)
788 {
789   int i;
790
791   for (i = 15; i >= 0; i--)
792   {
793     SetLaserColor(0x11 * i);
794
795     DrawLaser(0, DL_LASER_ENABLED);
796
797     BackToFront_MM();
798     Delay_WithScreenUpdates(50);
799   }
800
801   DrawLaser(0, DL_LASER_DISABLED);
802
803   StopSound_MM(SND_MM_GAME_HEALTH_CHARGING);
804 }
805
806 static void GameOver_MM(int game_over_cause)
807 {
808   game_mm.game_over = TRUE;
809   game_mm.game_over_cause = game_over_cause;
810   game_mm.game_over_message = (game_mm.has_mcduffin ?
811                                (game_over_cause == GAME_OVER_BOMB ?
812                                 "Bomb killed Mc Duffin!" :
813                                 game_over_cause == GAME_OVER_NO_ENERGY ?
814                                 "Out of magic energy!" :
815                                 game_over_cause == GAME_OVER_OVERLOADED ?
816                                 "Magic spell hit Mc Duffin!" :
817                                 NULL) :
818                                (game_over_cause == GAME_OVER_BOMB ?
819                                 "Bomb destroyed laser cannon!" :
820                                 game_over_cause == GAME_OVER_NO_ENERGY ?
821                                 "Out of laser energy!" :
822                                 game_over_cause == GAME_OVER_OVERLOADED ?
823                                 "Laser beam hit laser cannon!" :
824                                 NULL));
825
826   SetTileCursorActive(FALSE);
827 }
828
829 static void AddLaserEdge(int lx, int ly)
830 {
831   int clx = dSX + lx;
832   int cly = dSY + ly;
833   int sxsize = MAX(SXSIZE, lev_fieldx * TILEX);
834   int sysize = MAX(SYSIZE, lev_fieldy * TILEY);
835
836   if (clx < -2 || cly < -2 || clx >= sxsize + 2 || cly >= sysize + 2)
837   {
838     Warn("AddLaserEdge: out of bounds: %d, %d", lx, ly);
839
840     return;
841   }
842
843   laser.edge[laser.num_edges].x = cSX2 + lx;
844   laser.edge[laser.num_edges].y = cSY2 + ly;
845   laser.num_edges++;
846
847   laser.redraw = TRUE;
848 }
849
850 static void AddDamagedField(int ex, int ey)
851 {
852   // prevent adding the same field position again
853   if (laser.num_damages > 0 &&
854       laser.damage[laser.num_damages - 1].x == ex &&
855       laser.damage[laser.num_damages - 1].y == ey &&
856       laser.damage[laser.num_damages - 1].edge == laser.num_edges)
857     return;
858
859   laser.damage[laser.num_damages].is_mirror = FALSE;
860   laser.damage[laser.num_damages].angle = laser.current_angle;
861   laser.damage[laser.num_damages].edge = laser.num_edges;
862   laser.damage[laser.num_damages].x = ex;
863   laser.damage[laser.num_damages].y = ey;
864   laser.num_damages++;
865 }
866
867 static boolean StepBehind(void)
868 {
869   if (laser.num_edges)
870   {
871     int x = LX - XS;
872     int y = LY - YS;
873     int last_x = laser.edge[laser.num_edges - 1].x - cSX2;
874     int last_y = laser.edge[laser.num_edges - 1].y - cSY2;
875
876     return ((x - last_x) * XS < 0 || (y - last_y) * YS < 0);
877   }
878
879   return FALSE;
880 }
881
882 static int getMaskFromElement(int element)
883 {
884   if (IS_GRID(element))
885     return MM_MASK_GRID_1 + get_element_phase(element);
886   else if (IS_MCDUFFIN(element))
887     return MM_MASK_MCDUFFIN_RIGHT + get_element_phase(element);
888   else if (IS_RECTANGLE(element) || IS_DF_GRID(element))
889     return MM_MASK_RECTANGLE;
890   else
891     return MM_MASK_CIRCLE;
892 }
893
894 static int ScanPixel(void)
895 {
896   int hit_mask = 0;
897
898 #if 0
899   Debug("game:mm:ScanPixel", "start scanning at (%d, %d) [%d, %d] [%d, %d]",
900         LX, LY, LX / TILEX, LY / TILEY, LX % TILEX, LY % TILEY);
901 #endif
902
903   // follow laser beam until it hits something (at least the screen border)
904   while (hit_mask == HIT_MASK_NO_HIT)
905   {
906     int i;
907
908 #if 0
909     // for safety
910     if (SX + LX < REAL_SX || SX + LX >= REAL_SX + FULL_SXSIZE ||
911         SY + LY < REAL_SY || SY + LY >= REAL_SY + FULL_SYSIZE)
912     {
913       Debug("game:mm:ScanPixel", "touched screen border!");
914
915       return HIT_MASK_ALL;
916     }
917 #endif
918
919     // check if laser scan has crossed element boundaries (not just mini tiles)
920     boolean cross_x = (LX / TILEX != (LX + 2) / TILEX);
921     boolean cross_y = (LY / TILEY != (LY + 2) / TILEY);
922
923     if (cross_x && cross_y)
924     {
925       int elx1 = (LX - XS) / TILEX;
926       int ely1 = (LY + YS) / TILEY;
927       int elx2 = (LX + XS) / TILEX;
928       int ely2 = (LY - YS) / TILEY;
929
930       // add element corners left and right from the laser beam to damage list
931
932       if (IN_LEV_FIELD(elx1, ely1) && Tile[elx1][ely1] != EL_EMPTY)
933         AddDamagedField(elx1, ely1);
934
935       if (IN_LEV_FIELD(elx2, ely2) && Tile[elx2][ely2] != EL_EMPTY)
936         AddDamagedField(elx2, ely2);
937     }
938
939     for (i = 0; i < 4; i++)
940     {
941       int px = LX + (i % 2) * 2;
942       int py = LY + (i / 2) * 2;
943       int dx = px % TILEX;
944       int dy = py % TILEY;
945       int lx = (px + TILEX) / TILEX - 1;  // ...+TILEX...-1 to get correct
946       int ly = (py + TILEY) / TILEY - 1;  // negative values!
947       Pixel pixel;
948
949       if (IN_LEV_FIELD(lx, ly))
950       {
951         int element = Tile[lx][ly];
952
953         if (element == EL_EMPTY || element == EL_EXPLODING_TRANSP)
954         {
955           pixel = 0;
956         }
957         else if (IS_WALL(element) || IS_WALL_CHANGING(element))
958         {
959           int pos = dy / MINI_TILEY * 2 + dx / MINI_TILEX;
960
961           pixel = ((element & (1 << pos)) ? 1 : 0);
962         }
963         else
964         {
965           int pos = getMaskFromElement(element);
966
967           pixel = (mm_masks[pos][dy / 2][dx / 2] == 'X' ? 1 : 0);
968         }
969       }
970       else
971       {
972         pixel = (cSX + px < REAL_SX || cSX + px >= REAL_SX + FULL_SXSIZE ||
973                  cSY + py < REAL_SY || cSY + py >= REAL_SY + FULL_SYSIZE);
974       }
975
976       if ((Sign[laser.current_angle] & (1 << i)) && pixel)
977         hit_mask |= (1 << i);
978     }
979
980     if (hit_mask == HIT_MASK_NO_HIT)
981     {
982       // hit nothing -- go on with another step
983       LX += XS;
984       LY += YS;
985     }
986   }
987
988   return hit_mask;
989 }
990
991 static void DeactivateLaserTargetElement(void)
992 {
993   if (laser.dest_element_last == EL_BOMB_ACTIVE ||
994       laser.dest_element_last == EL_MINE_ACTIVE ||
995       laser.dest_element_last == EL_GRAY_BALL_ACTIVE ||
996       laser.dest_element_last == EL_GRAY_BALL_OPENING)
997   {
998     int x = laser.dest_element_last_x;
999     int y = laser.dest_element_last_y;
1000     int element = laser.dest_element_last;
1001
1002     if (Tile[x][y] == element)
1003       Tile[x][y] = (element == EL_BOMB_ACTIVE ? EL_BOMB :
1004                     element == EL_MINE_ACTIVE ? EL_MINE : EL_GRAY_BALL);
1005
1006     if (Tile[x][y] == EL_GRAY_BALL)
1007       MovDelay[x][y] = 0;
1008
1009     laser.dest_element_last = EL_EMPTY;
1010     laser.dest_element_last_x = -1;
1011     laser.dest_element_last_y = -1;
1012   }
1013 }
1014
1015 static void ScanLaser(void)
1016 {
1017   int element = EL_EMPTY;
1018   int last_element = EL_EMPTY;
1019   int end = 0, rf = laser.num_edges;
1020
1021   // do not scan laser again after the game was lost for whatever reason
1022   if (game_mm.game_over)
1023     return;
1024
1025   // do not scan laser if fuse is off
1026   if (laser.fuse_off)
1027     return;
1028
1029   DeactivateLaserTargetElement();
1030
1031   laser.overloaded = FALSE;
1032   laser.stops_inside_element = FALSE;
1033
1034   DrawLaser(0, DL_LASER_ENABLED);
1035
1036 #if 0
1037   Debug("game:mm:ScanLaser",
1038         "Start scanning with LX == %d, LY == %d, XS == %d, YS == %d",
1039         LX, LY, XS, YS);
1040 #endif
1041
1042   while (1)
1043   {
1044     int hit_mask;
1045
1046     if (laser.num_edges > MAX_LASER_LEN || laser.num_damages > MAX_LASER_LEN)
1047     {
1048       end = 1;
1049       laser.overloaded = TRUE;
1050
1051       break;
1052     }
1053
1054     hit_mask = ScanPixel();
1055
1056 #if 0
1057     Debug("game:mm:ScanLaser",
1058           "Hit something at LX == %d, LY == %d, XS == %d, YS == %d",
1059           LX, LY, XS, YS);
1060 #endif
1061
1062     // hit something -- check out what it was
1063     ELX = (LX + XS + TILEX) / TILEX - 1;  // ...+TILEX...-1 to get correct
1064     ELY = (LY + YS + TILEY) / TILEY - 1;  // negative values!
1065
1066 #if 0
1067     Debug("game:mm:ScanLaser", "hit_mask (1) == '%x' (%d, %d) (%d, %d)",
1068           hit_mask, LX, LY, ELX, ELY);
1069 #endif
1070
1071     if (!IN_LEV_FIELD(ELX, ELY))
1072     {
1073       element = EL_EMPTY;
1074       laser.dest_element = element;
1075
1076       break;
1077     }
1078
1079     // check if laser scan has hit two diagonally adjacent element corners
1080     boolean diag_1 = ((hit_mask & HIT_MASK_DIAGONAL_1) == HIT_MASK_DIAGONAL_1);
1081     boolean diag_2 = ((hit_mask & HIT_MASK_DIAGONAL_2) == HIT_MASK_DIAGONAL_2);
1082
1083     // check if laser scan has crossed element boundaries (not just mini tiles)
1084     boolean cross_x = (LX / TILEX != (LX + 2) / TILEX);
1085     boolean cross_y = (LY / TILEY != (LY + 2) / TILEY);
1086
1087     // handle special case of laser hitting two diagonally adjacent elements
1088     // (with or without a third corner element behind these two elements)
1089     if ((diag_1 || diag_2) && cross_x && cross_y)
1090     {
1091       // compare the two diagonally adjacent elements
1092       int xoffset = 2;
1093       int yoffset = 2 * (diag_1 ? -1 : +1);
1094       int elx1 = (LX - xoffset) / TILEX;
1095       int ely1 = (LY + yoffset) / TILEY;
1096       int elx2 = (LX + xoffset) / TILEX;
1097       int ely2 = (LY - yoffset) / TILEY;
1098       int e1 = Tile[elx1][ely1];
1099       int e2 = Tile[elx2][ely2];
1100       boolean use_element_1 = FALSE;
1101
1102       if (IS_WALL_ICE(e1) || IS_WALL_ICE(e2))
1103       {
1104         if (IS_WALL_ICE(e1) && IS_WALL_ICE(e2))
1105           use_element_1 = (RND(2) ? TRUE : FALSE);
1106         else if (IS_WALL_ICE(e1))
1107           use_element_1 = TRUE;
1108       }
1109       else if (IS_WALL_AMOEBA(e1) || IS_WALL_AMOEBA(e2))
1110       {
1111         // if both tiles match, we can just select the first one
1112         if (IS_WALL_AMOEBA(e1))
1113           use_element_1 = TRUE;
1114       }
1115       else if (IS_ABSORBING_BLOCK(e1) || IS_ABSORBING_BLOCK(e2))
1116       {
1117         // if both tiles match, we can just select the first one
1118         if (IS_ABSORBING_BLOCK(e1))
1119           use_element_1 = TRUE;
1120       }
1121
1122       ELX = (use_element_1 ? elx1 : elx2);
1123       ELY = (use_element_1 ? ely1 : ely2);
1124     }
1125
1126 #if 0
1127     Debug("game:mm:ScanLaser", "hit_mask (2) == '%x' (%d, %d) (%d, %d)",
1128           hit_mask, LX, LY, ELX, ELY);
1129 #endif
1130
1131     last_element = element;
1132
1133     element = Tile[ELX][ELY];
1134     laser.dest_element = element;
1135
1136 #if 0
1137     Debug("game:mm:ScanLaser",
1138           "Hit element %d at (%d, %d) [%d, %d] [%d, %d] [%d]",
1139           element, ELX, ELY,
1140           LX, LY,
1141           LX % TILEX, LY % TILEY,
1142           hit_mask);
1143 #endif
1144
1145 #if 0
1146     if (!IN_LEV_FIELD(ELX, ELY))
1147       Debug("game:mm:ScanLaser", "WARNING! (1) %d, %d (%d)",
1148             ELX, ELY, element);
1149 #endif
1150
1151     // special case: leaving fixed MM steel grid (upwards) with non-90° angle
1152     if (element == EL_EMPTY &&
1153         IS_GRID_STEEL(last_element) &&
1154         laser.current_angle % 4)                // angle is not 90°
1155       element = last_element;
1156
1157     if (element == EL_EMPTY)
1158     {
1159       if (!HitOnlyAnEdge(hit_mask))
1160         break;
1161     }
1162     else if (element == EL_FUSE_ON)
1163     {
1164       if (HitPolarizer(element, hit_mask))
1165         break;
1166     }
1167     else if (IS_GRID(element) || IS_DF_GRID(element))
1168     {
1169       if (HitPolarizer(element, hit_mask))
1170         break;
1171     }
1172     else if (element == EL_BLOCK_STONE || element == EL_BLOCK_WOOD ||
1173              element == EL_GATE_STONE || element == EL_GATE_WOOD)
1174     {
1175       if (HitBlock(element, hit_mask))
1176       {
1177         rf = 1;
1178
1179         break;
1180       }
1181     }
1182     else if (IS_MCDUFFIN(element))
1183     {
1184       if (HitLaserSource(element, hit_mask))
1185         break;
1186     }
1187     else if ((element >= EL_EXIT_CLOSED && element <= EL_EXIT_OPEN) ||
1188              IS_RECEIVER(element))
1189     {
1190       if (HitLaserDestination(element, hit_mask))
1191         break;
1192     }
1193     else if (IS_WALL(element))
1194     {
1195       if (IS_WALL_STEEL(element) || IS_DF_WALL_STEEL(element))
1196       {
1197         if (HitReflectingWalls(element, hit_mask))
1198           break;
1199       }
1200       else
1201       {
1202         if (HitAbsorbingWalls(element, hit_mask))
1203           break;
1204       }
1205     }
1206     else
1207     {
1208       if (HitElement(element, hit_mask))
1209         break;
1210     }
1211
1212     if (rf)
1213       DrawLaser(rf - 1, DL_LASER_ENABLED);
1214     rf = laser.num_edges;
1215
1216     if (!IS_DF_WALL_STEEL(element))
1217     {
1218       // only used for scanning DF steel walls; reset for all other elements
1219       last_LX = 0;
1220       last_LY = 0;
1221       last_hit_mask = 0;
1222     }
1223   }
1224
1225 #if 0
1226   if (laser.dest_element != Tile[ELX][ELY])
1227   {
1228     Debug("game:mm:ScanLaser",
1229           "ALARM: laser.dest_element == %d, Tile[ELX][ELY] == %d",
1230           laser.dest_element, Tile[ELX][ELY]);
1231   }
1232 #endif
1233
1234   if (!end && !laser.stops_inside_element && !StepBehind())
1235   {
1236 #if 0
1237     Debug("game:mm:ScanLaser", "Go one step back");
1238 #endif
1239
1240     LX -= XS;
1241     LY -= YS;
1242
1243     AddLaserEdge(LX, LY);
1244   }
1245
1246   if (rf)
1247     DrawLaser(rf - 1, DL_LASER_ENABLED);
1248
1249   Ct = CT = FrameCounter;
1250
1251 #if 0
1252     if (!IN_LEV_FIELD(ELX, ELY))
1253       Debug("game:mm:ScanLaser", "WARNING! (2) %d, %d", ELX, ELY);
1254 #endif
1255 }
1256
1257 static void ScanLaser_FromLastMirror(void)
1258 {
1259   int start_pos = (laser.num_damages > 0 ? laser.num_damages - 1 : 0);
1260   int i;
1261
1262   for (i = start_pos; i >= 0; i--)
1263     if (laser.damage[i].is_mirror)
1264       break;
1265
1266   int start_edge = (i > 0 ? laser.damage[i].edge - 1 : 0);
1267
1268   DrawLaser(start_edge, DL_LASER_DISABLED);
1269
1270   ScanLaser();
1271 }
1272
1273 static void DrawLaserExt(int start_edge, int num_edges, int mode)
1274 {
1275   int element;
1276   int elx, ely;
1277
1278 #if 0
1279   Debug("game:mm:DrawLaserExt", "start_edge, num_edges, mode == %d, %d, %d",
1280         start_edge, num_edges, mode);
1281 #endif
1282
1283   if (start_edge < 0)
1284   {
1285     Warn("DrawLaserExt: start_edge < 0");
1286
1287     return;
1288   }
1289
1290   if (num_edges < 0)
1291   {
1292     Warn("DrawLaserExt: num_edges < 0");
1293
1294     return;
1295   }
1296
1297 #if 0
1298   if (mode == DL_LASER_DISABLED)
1299   {
1300     Debug("game:mm:DrawLaserExt", "Delete laser from edge %d", start_edge);
1301   }
1302 #endif
1303
1304   // now draw the laser to the backbuffer and (if enabled) to the screen
1305   DrawLaserLines(&laser.edge[start_edge], num_edges, mode);
1306
1307   redraw_mask |= REDRAW_FIELD;
1308
1309   if (mode == DL_LASER_ENABLED)
1310     return;
1311
1312   // after the laser was deleted, the "damaged" graphics must be restored
1313   if (laser.num_damages)
1314   {
1315     int damage_start = 0;
1316     int i;
1317
1318     // determine the starting edge, from which graphics need to be restored
1319     if (start_edge > 0)
1320     {
1321       for (i = 0; i < laser.num_damages; i++)
1322       {
1323         if (laser.damage[i].edge == start_edge + 1)
1324         {
1325           damage_start = i;
1326
1327           break;
1328         }
1329       }
1330     }
1331
1332     // restore graphics from this starting edge to the end of damage list
1333     for (i = damage_start; i < laser.num_damages; i++)
1334     {
1335       int lx = laser.damage[i].x;
1336       int ly = laser.damage[i].y;
1337       int element = Tile[lx][ly];
1338
1339       if (Hit[lx][ly] == laser.damage[i].edge)
1340         if (!((IS_BEAMER(element) || IS_FIBRE_OPTIC(element)) &&
1341                i == damage_start))
1342           Hit[lx][ly] = 0;
1343       if (Box[lx][ly] == laser.damage[i].edge)
1344         Box[lx][ly] = 0;
1345
1346       if (IS_DRAWABLE(element))
1347         DrawField_MM(lx, ly);
1348     }
1349
1350     elx = laser.damage[damage_start].x;
1351     ely = laser.damage[damage_start].y;
1352     element = Tile[elx][ely];
1353
1354 #if 0
1355     if (IS_BEAMER(element))
1356     {
1357       int i;
1358
1359       for (i = 0; i < laser.num_beamers; i++)
1360         Debug("game:mm:DrawLaserExt", "-> %d", laser.beamer_edge[i]);
1361
1362       Debug("game:mm:DrawLaserExt", "IS_BEAMER: [%d]: Hit[%d][%d] == %d [%d]",
1363             mode, elx, ely, Hit[elx][ely], start_edge);
1364       Debug("game:mm:DrawLaserExt", "IS_BEAMER: %d / %d",
1365             get_element_angle(element), laser.damage[damage_start].angle);
1366     }
1367 #endif
1368
1369     if ((IS_BEAMER(element) || IS_FIBRE_OPTIC(element)) &&
1370         laser.num_beamers > 0 &&
1371         start_edge == laser.beamer_edge[laser.num_beamers - 1])
1372     {
1373       // element is outgoing beamer
1374       laser.num_damages = damage_start + 1;
1375
1376       if (IS_BEAMER(element))
1377         laser.current_angle = get_element_angle(element);
1378     }
1379     else
1380     {
1381       // element is incoming beamer or other element
1382       laser.num_damages = damage_start;
1383       laser.current_angle = laser.damage[laser.num_damages].angle;
1384     }
1385   }
1386   else
1387   {
1388     // no damages but McDuffin himself (who needs to be redrawn anyway)
1389
1390     elx = laser.start_edge.x;
1391     ely = laser.start_edge.y;
1392     element = Tile[elx][ely];
1393   }
1394
1395   laser.num_edges = start_edge + 1;
1396   if (start_edge == 0)
1397     laser.current_angle = laser.start_angle;
1398
1399   LX = laser.edge[start_edge].x - cSX2;
1400   LY = laser.edge[start_edge].y - cSY2;
1401   XS = 2 * Step[laser.current_angle].x;
1402   YS = 2 * Step[laser.current_angle].y;
1403
1404 #if 0
1405   Debug("game:mm:DrawLaserExt", "Set (LX, LY) to (%d, %d) [%d]",
1406         LX, LY, element);
1407 #endif
1408
1409   if (start_edge > 0)
1410   {
1411     if (IS_BEAMER(element) ||
1412         IS_FIBRE_OPTIC(element) ||
1413         IS_PACMAN(element) ||
1414         IS_POLAR(element) ||
1415         IS_POLAR_CROSS(element) ||
1416         element == EL_FUSE_ON)
1417     {
1418       int step_size;
1419
1420 #if 0
1421       Debug("game:mm:DrawLaserExt", "element == %d", element);
1422 #endif
1423
1424       if (IS_22_5_ANGLE(laser.current_angle))   // neither 90° nor 45° angle
1425         step_size = ((IS_BEAMER(element) || IS_FIBRE_OPTIC(element)) ? 4 : 3);
1426       else
1427         step_size = 8;
1428
1429       if (IS_POLAR(element) || IS_POLAR_CROSS(element) ||
1430           ((IS_BEAMER(element) || IS_FIBRE_OPTIC(element)) &&
1431            (laser.num_beamers == 0 ||
1432             start_edge != laser.beamer_edge[laser.num_beamers - 1])))
1433       {
1434         // element is incoming beamer or other element
1435         step_size = -step_size;
1436         laser.num_edges--;
1437       }
1438
1439 #if 0
1440       if (IS_BEAMER(element))
1441         Debug("game:mm:DrawLaserExt",
1442               "start_edge == %d, laser.beamer_edge == %d",
1443               start_edge, laser.beamer_edge);
1444 #endif
1445
1446       LX += step_size * XS;
1447       LY += step_size * YS;
1448     }
1449     else if (element != EL_EMPTY)
1450     {
1451       LX -= 3 * XS;
1452       LY -= 3 * YS;
1453       laser.num_edges--;
1454     }
1455   }
1456
1457 #if 0
1458   Debug("game:mm:DrawLaserExt", "Finally: (LX, LY) to (%d, %d) [%d]",
1459         LX, LY, element);
1460 #endif
1461 }
1462
1463 void DrawLaser(int start_edge, int mode)
1464 {
1465   // do not draw laser if fuse is off
1466   if (laser.fuse_off && mode == DL_LASER_ENABLED)
1467     return;
1468
1469   if (mode == DL_LASER_DISABLED)
1470     DeactivateLaserTargetElement();
1471
1472   if (laser.num_edges - start_edge < 0)
1473   {
1474     Warn("DrawLaser: laser.num_edges - start_edge < 0");
1475
1476     return;
1477   }
1478
1479   // check if laser is interrupted by beamer element
1480   if (laser.num_beamers > 0 &&
1481       start_edge < laser.beamer_edge[laser.num_beamers - 1])
1482   {
1483     if (mode == DL_LASER_ENABLED)
1484     {
1485       int i;
1486       int tmp_start_edge = start_edge;
1487
1488       // draw laser segments forward from the start to the last beamer
1489       for (i = 0; i < laser.num_beamers; i++)
1490       {
1491         int tmp_num_edges = laser.beamer_edge[i] - tmp_start_edge;
1492
1493         if (tmp_num_edges <= 0)
1494           continue;
1495
1496 #if 0
1497         Debug("game:mm:DrawLaser", "DL_LASER_ENABLED: i==%d: %d, %d",
1498               i, laser.beamer_edge[i], tmp_start_edge);
1499 #endif
1500
1501         DrawLaserExt(tmp_start_edge, tmp_num_edges, DL_LASER_ENABLED);
1502
1503         tmp_start_edge = laser.beamer_edge[i];
1504       }
1505
1506       // draw last segment from last beamer to the end
1507       DrawLaserExt(tmp_start_edge, laser.num_edges - tmp_start_edge,
1508                    DL_LASER_ENABLED);
1509     }
1510     else
1511     {
1512       int i;
1513       int last_num_edges = laser.num_edges;
1514       int num_beamers = laser.num_beamers;
1515
1516       // delete laser segments backward from the end to the first beamer
1517       for (i = num_beamers - 1; i >= 0; i--)
1518       {
1519         int tmp_num_edges = last_num_edges - laser.beamer_edge[i];
1520
1521         if (laser.beamer_edge[i] - start_edge <= 0)
1522           break;
1523
1524         DrawLaserExt(laser.beamer_edge[i], tmp_num_edges, DL_LASER_DISABLED);
1525
1526         last_num_edges = laser.beamer_edge[i];
1527         laser.num_beamers--;
1528       }
1529
1530 #if 0
1531       if (last_num_edges - start_edge <= 0)
1532         Debug("game:mm:DrawLaser", "DL_LASER_DISABLED: %d, %d",
1533               last_num_edges, start_edge);
1534 #endif
1535
1536       // special case when rotating first beamer: delete laser edge on beamer
1537       // (but do not start scanning on previous edge to prevent mirror sound)
1538       if (last_num_edges - start_edge == 1 && start_edge > 0)
1539         DrawLaserLines(&laser.edge[start_edge - 1], 2, DL_LASER_DISABLED);
1540
1541       // delete first segment from start to the first beamer
1542       DrawLaserExt(start_edge, last_num_edges - start_edge, DL_LASER_DISABLED);
1543     }
1544   }
1545   else
1546   {
1547     DrawLaserExt(start_edge, laser.num_edges - start_edge, mode);
1548   }
1549
1550   game_mm.laser_enabled = mode;
1551 }
1552
1553 void DrawLaser_MM(void)
1554 {
1555   DrawLaser(0, game_mm.laser_enabled);
1556 }
1557
1558 static boolean HitElement(int element, int hit_mask)
1559 {
1560   if (HitOnlyAnEdge(hit_mask))
1561     return FALSE;
1562
1563   if (IS_MOVING(ELX, ELY) || IS_BLOCKED(ELX, ELY))
1564     element = MovingOrBlocked2Element_MM(ELX, ELY);
1565
1566 #if 0
1567   Debug("game:mm:HitElement", "(1): element == %d", element);
1568 #endif
1569
1570 #if 0
1571   if ((ELX * TILEX + 14 - LX) * YS == (ELY * TILEY + 14 - LY) * XS)
1572     Debug("game:mm:HitElement", "(%d): EXACT MATCH @ (%d, %d)",
1573           element, ELX, ELY);
1574   else
1575     Debug("game:mm:HitElement", "(%d): FUZZY MATCH @ (%d, %d)",
1576           element, ELX, ELY);
1577 #endif
1578
1579   AddDamagedField(ELX, ELY);
1580
1581   // this is more precise: check if laser would go through the center
1582   if ((ELX * TILEX + 14 - LX) * YS != (ELY * TILEY + 14 - LY) * XS)
1583   {
1584     int skip_count = 0;
1585
1586     // prevent cutting through laser emitter with laser beam
1587     if (IS_LASER(element))
1588       return TRUE;
1589
1590     // skip the whole element before continuing the scan
1591     do
1592     {
1593       LX += XS;
1594       LY += YS;
1595
1596       skip_count++;
1597     }
1598     while (ELX == LX/TILEX && ELY == LY/TILEY && LX > 0 && LY > 0);
1599
1600     if ((LX/TILEX > ELX || LY/TILEY > ELY) && skip_count > 1)
1601     {
1602       /* skipping scan positions to the right and down skips one scan
1603          position too much, because this is only the top left scan position
1604          of totally four scan positions (plus one to the right, one to the
1605          bottom and one to the bottom right) */
1606       /* ... but only roll back scan position if more than one step done */
1607
1608       LX -= XS;
1609       LY -= YS;
1610     }
1611
1612     return FALSE;
1613   }
1614
1615 #if 0
1616   Debug("game:mm:HitElement", "(2): element == %d", element);
1617 #endif
1618
1619   if (LX + 5 * XS < 0 ||
1620       LY + 5 * YS < 0)
1621   {
1622     LX += 2 * XS;
1623     LY += 2 * YS;
1624
1625     return FALSE;
1626   }
1627
1628 #if 0
1629   Debug("game:mm:HitElement", "(3): element == %d", element);
1630 #endif
1631
1632   if (IS_POLAR(element) &&
1633       ((element - EL_POLAR_START) % 2 ||
1634        (element - EL_POLAR_START) / 2 != laser.current_angle % 8))
1635   {
1636     PlayLevelSound_MM(ELX, ELY, element, MM_ACTION_HITTING);
1637
1638     laser.num_damages--;
1639
1640     return TRUE;
1641   }
1642
1643   if (IS_POLAR_CROSS(element) &&
1644       (element - EL_POLAR_CROSS_START) != laser.current_angle % 4)
1645   {
1646     PlayLevelSound_MM(ELX, ELY, element, MM_ACTION_HITTING);
1647
1648     laser.num_damages--;
1649
1650     return TRUE;
1651   }
1652
1653   if (!IS_BEAMER(element) &&
1654       !IS_FIBRE_OPTIC(element) &&
1655       !IS_GRID_WOOD(element) &&
1656       element != EL_FUEL_EMPTY)
1657   {
1658 #if 0
1659     if ((ELX * TILEX + 14 - LX) * YS == (ELY * TILEY + 14 - LY) * XS)
1660       Debug("game:mm:HitElement", "EXACT MATCH @ (%d, %d)", ELX, ELY);
1661     else
1662       Debug("game:mm:HitElement", "FUZZY MATCH @ (%d, %d)", ELX, ELY);
1663 #endif
1664
1665     LX = ELX * TILEX + 14;
1666     LY = ELY * TILEY + 14;
1667
1668     AddLaserEdge(LX, LY);
1669   }
1670
1671   if (IS_MIRROR(element) ||
1672       IS_MIRROR_FIXED(element) ||
1673       IS_POLAR(element) ||
1674       IS_POLAR_CROSS(element) ||
1675       IS_DF_MIRROR(element) ||
1676       IS_DF_MIRROR_AUTO(element) ||
1677       element == EL_PRISM ||
1678       element == EL_REFRACTOR)
1679   {
1680     int current_angle = laser.current_angle;
1681     int step_size;
1682
1683     laser.num_damages--;
1684
1685     AddDamagedField(ELX, ELY);
1686
1687     laser.damage[laser.num_damages - 1].is_mirror = TRUE;
1688
1689     if (!Hit[ELX][ELY])
1690       Hit[ELX][ELY] = laser.damage[laser.num_damages - 1].edge;
1691
1692     if (IS_MIRROR(element) ||
1693         IS_MIRROR_FIXED(element) ||
1694         IS_DF_MIRROR(element) ||
1695         IS_DF_MIRROR_AUTO(element))
1696       laser.current_angle = get_mirrored_angle(laser.current_angle,
1697                                                get_element_angle(element));
1698
1699     if (element == EL_PRISM || element == EL_REFRACTOR)
1700       laser.current_angle = RND(16);
1701
1702     XS = 2 * Step[laser.current_angle].x;
1703     YS = 2 * Step[laser.current_angle].y;
1704
1705     if (!IS_22_5_ANGLE(laser.current_angle))    // 90° or 45° angle
1706       step_size = 8;
1707     else
1708       step_size = 4;
1709
1710     LX += step_size * XS;
1711     LY += step_size * YS;
1712
1713     // draw sparkles on mirror
1714     if ((IS_MIRROR(element) ||
1715          IS_MIRROR_FIXED(element) ||
1716          element == EL_PRISM) &&
1717         current_angle != laser.current_angle)
1718     {
1719       MovDelay[ELX][ELY] = 11;          // start animation
1720     }
1721
1722     if ((!IS_POLAR(element) && !IS_POLAR_CROSS(element)) &&
1723         current_angle != laser.current_angle)
1724       PlayLevelSound_MM(ELX, ELY, element, MM_ACTION_HITTING);
1725
1726     laser.overloaded =
1727       (get_opposite_angle(laser.current_angle) ==
1728        laser.damage[laser.num_damages - 1].angle ? TRUE : FALSE);
1729
1730     return (laser.overloaded ? TRUE : FALSE);
1731   }
1732
1733   if (element == EL_FUEL_FULL)
1734   {
1735     laser.stops_inside_element = TRUE;
1736
1737     return TRUE;
1738   }
1739
1740   if (element == EL_BOMB || element == EL_MINE || element == EL_GRAY_BALL)
1741   {
1742     PlayLevelSound_MM(ELX, ELY, element, MM_ACTION_HITTING);
1743
1744     Tile[ELX][ELY] = (element == EL_BOMB ? EL_BOMB_ACTIVE :
1745                       element == EL_MINE ? EL_MINE_ACTIVE :
1746                       EL_GRAY_BALL_ACTIVE);
1747
1748     GfxFrame[ELX][ELY] = 0;             // restart animation
1749
1750     laser.dest_element_last = Tile[ELX][ELY];
1751     laser.dest_element_last_x = ELX;
1752     laser.dest_element_last_y = ELY;
1753
1754     if (element == EL_MINE)
1755       laser.overloaded = TRUE;
1756   }
1757
1758   if (element == EL_KETTLE ||
1759       element == EL_CELL ||
1760       element == EL_KEY ||
1761       element == EL_LIGHTBALL ||
1762       element == EL_PACMAN ||
1763       IS_PACMAN(element) ||
1764       IS_ENVELOPE(element))
1765   {
1766     if (!IS_PACMAN(element) &&
1767         !IS_ENVELOPE(element))
1768       Bang_MM(ELX, ELY);
1769
1770     if (element == EL_PACMAN)
1771       Bang_MM(ELX, ELY);
1772
1773     if (element == EL_KETTLE || element == EL_CELL)
1774     {
1775       if (game_mm.kettles_still_needed > 0)
1776         game_mm.kettles_still_needed--;
1777
1778       game.snapshot.collected_item = TRUE;
1779
1780       if (game_mm.kettles_still_needed == 0)
1781       {
1782         CheckExitMM();
1783
1784         DrawLaser(0, DL_LASER_ENABLED);
1785       }
1786     }
1787     else if (element == EL_KEY)
1788     {
1789       game_mm.num_keys++;
1790     }
1791     else if (IS_PACMAN(element))
1792     {
1793       DeletePacMan(ELX, ELY);
1794     }
1795     else if (IS_ENVELOPE(element))
1796     {
1797       Tile[ELX][ELY] = EL_ENVELOPE_1_OPENING + ENVELOPE_NR(Tile[ELX][ELY]);
1798     }
1799
1800     RaiseScoreElement_MM(element);
1801
1802     return FALSE;
1803   }
1804
1805   if (element == EL_LIGHTBULB_OFF || element == EL_LIGHTBULB_ON)
1806   {
1807     PlayLevelSound_MM(ELX, ELY, element, MM_ACTION_HITTING);
1808
1809     DrawLaser(0, DL_LASER_ENABLED);
1810
1811     if (Tile[ELX][ELY] == EL_LIGHTBULB_OFF)
1812     {
1813       Tile[ELX][ELY] = EL_LIGHTBULB_ON;
1814       game_mm.lights_still_needed--;
1815     }
1816     else
1817     {
1818       Tile[ELX][ELY] = EL_LIGHTBULB_OFF;
1819       game_mm.lights_still_needed++;
1820     }
1821
1822     DrawField_MM(ELX, ELY);
1823     DrawLaser(0, DL_LASER_ENABLED);
1824
1825     /*
1826     BackToFront();
1827     */
1828     laser.stops_inside_element = TRUE;
1829
1830     return TRUE;
1831   }
1832
1833 #if 0
1834   Debug("game:mm:HitElement", "(4): element == %d", element);
1835 #endif
1836
1837   if ((IS_BEAMER(element) || IS_FIBRE_OPTIC(element)) &&
1838       laser.num_beamers < MAX_NUM_BEAMERS &&
1839       laser.beamer[BEAMER_NR(element)][1].num)
1840   {
1841     int beamer_angle = get_element_angle(element);
1842     int beamer_nr = BEAMER_NR(element);
1843     int step_size;
1844
1845 #if 0
1846     Debug("game:mm:HitElement", "(BEAMER): element == %d", element);
1847 #endif
1848
1849     laser.num_damages--;
1850
1851     if (IS_FIBRE_OPTIC(element) ||
1852         laser.current_angle == get_opposite_angle(beamer_angle))
1853     {
1854       int pos;
1855
1856       LX = ELX * TILEX + 14;
1857       LY = ELY * TILEY + 14;
1858
1859       AddLaserEdge(LX, LY);
1860       AddDamagedField(ELX, ELY);
1861
1862       laser.damage[laser.num_damages - 1].is_mirror = TRUE;
1863
1864       if (!Hit[ELX][ELY])
1865         Hit[ELX][ELY] = laser.damage[laser.num_damages - 1].edge;
1866
1867       pos = (ELX == laser.beamer[beamer_nr][0].x &&
1868              ELY == laser.beamer[beamer_nr][0].y ? 1 : 0);
1869       ELX = laser.beamer[beamer_nr][pos].x;
1870       ELY = laser.beamer[beamer_nr][pos].y;
1871       LX = ELX * TILEX + 14;
1872       LY = ELY * TILEY + 14;
1873
1874       if (IS_BEAMER(element))
1875       {
1876         laser.current_angle = get_element_angle(Tile[ELX][ELY]);
1877         XS = 2 * Step[laser.current_angle].x;
1878         YS = 2 * Step[laser.current_angle].y;
1879       }
1880
1881       laser.beamer_edge[laser.num_beamers] = laser.num_edges;
1882
1883       AddLaserEdge(LX, LY);
1884       AddDamagedField(ELX, ELY);
1885
1886       laser.damage[laser.num_damages - 1].is_mirror = TRUE;
1887
1888       if (!Hit[ELX][ELY])
1889         Hit[ELX][ELY] = laser.damage[laser.num_damages - 1].edge;
1890
1891       if (laser.current_angle == (laser.current_angle >> 1) << 1)
1892         step_size = 8;
1893       else
1894         step_size = 4;
1895
1896       LX += step_size * XS;
1897       LY += step_size * YS;
1898
1899       laser.num_beamers++;
1900
1901       return FALSE;
1902     }
1903   }
1904
1905   return TRUE;
1906 }
1907
1908 static boolean HitOnlyAnEdge(int hit_mask)
1909 {
1910   // check if the laser hit only the edge of an element and, if so, go on
1911
1912 #if 0
1913   Debug("game:mm:HitOnlyAnEdge", "LX, LY, hit_mask == %d, %d, %d",
1914         LX, LY, hit_mask);
1915 #endif
1916
1917   if ((hit_mask == HIT_MASK_TOPLEFT ||
1918        hit_mask == HIT_MASK_TOPRIGHT ||
1919        hit_mask == HIT_MASK_BOTTOMLEFT ||
1920        hit_mask == HIT_MASK_BOTTOMRIGHT) &&
1921       laser.current_angle % 4)                  // angle is not 90°
1922   {
1923     int dx, dy;
1924
1925     if (hit_mask == HIT_MASK_TOPLEFT)
1926     {
1927       dx = -1;
1928       dy = -1;
1929     }
1930     else if (hit_mask == HIT_MASK_TOPRIGHT)
1931     {
1932       dx = +1;
1933       dy = -1;
1934     }
1935     else if (hit_mask == HIT_MASK_BOTTOMLEFT)
1936     {
1937       dx = -1;
1938       dy = +1;
1939     }
1940     else // (hit_mask == HIT_MASK_BOTTOMRIGHT)
1941     {
1942       dx = +1;
1943       dy = +1;
1944     }
1945
1946     AddDamagedField((LX + 2 * dx) / TILEX, (LY + 2 * dy) / TILEY);
1947
1948     LX += XS;
1949     LY += YS;
1950
1951 #if 0
1952     Debug("game:mm:HitOnlyAnEdge", "[HitOnlyAnEdge() == TRUE]");
1953 #endif
1954
1955     return TRUE;
1956   }
1957
1958 #if 0
1959   Debug("game:mm:HitOnlyAnEdge", "[HitOnlyAnEdge() == FALSE]");
1960 #endif
1961
1962   return FALSE;
1963 }
1964
1965 static boolean HitPolarizer(int element, int hit_mask)
1966 {
1967   if (HitOnlyAnEdge(hit_mask))
1968     return FALSE;
1969
1970   if (IS_DF_GRID(element))
1971   {
1972     int grid_angle = get_element_angle(element);
1973
1974 #if 0
1975     Debug("game:mm:HitPolarizer", "angle: grid == %d, laser == %d",
1976           grid_angle, laser.current_angle);
1977 #endif
1978
1979     AddLaserEdge(LX, LY);
1980     AddDamagedField(ELX, ELY);
1981
1982     if (!Hit[ELX][ELY])
1983       Hit[ELX][ELY] = laser.damage[laser.num_damages - 1].edge;
1984
1985     if (laser.current_angle == grid_angle ||
1986         laser.current_angle == get_opposite_angle(grid_angle))
1987     {
1988       // skip the whole element before continuing the scan
1989       do
1990       {
1991         LX += XS;
1992         LY += YS;
1993       }
1994       while (ELX == LX/TILEX && ELY == LY/TILEY && LX > 0 && LY > 0);
1995
1996       if (LX/TILEX > ELX || LY/TILEY > ELY)
1997       {
1998         /* skipping scan positions to the right and down skips one scan
1999            position too much, because this is only the top left scan position
2000            of totally four scan positions (plus one to the right, one to the
2001            bottom and one to the bottom right) */
2002
2003         LX -= XS;
2004         LY -= YS;
2005       }
2006
2007       AddLaserEdge(LX, LY);
2008
2009       LX += XS;
2010       LY += YS;
2011
2012 #if 0
2013       Debug("game:mm:HitPolarizer", "LX, LY == %d, %d [%d, %d] [%d, %d]",
2014             LX, LY,
2015             LX / TILEX, LY / TILEY,
2016             LX % TILEX, LY % TILEY);
2017 #endif
2018
2019       return FALSE;
2020     }
2021     else if (IS_GRID_STEEL_FIXED(element) || IS_GRID_STEEL_AUTO(element))
2022     {
2023       return HitReflectingWalls(element, hit_mask);
2024     }
2025     else
2026     {
2027       return HitAbsorbingWalls(element, hit_mask);
2028     }
2029   }
2030   else if (IS_GRID_STEEL(element))
2031   {
2032     // may be required if graphics for steel grid redefined
2033     AddDamagedField(ELX, ELY);
2034
2035     return HitReflectingWalls(element, hit_mask);
2036   }
2037   else  // IS_GRID_WOOD
2038   {
2039     // may be required if graphics for wooden grid redefined
2040     AddDamagedField(ELX, ELY);
2041
2042     return HitAbsorbingWalls(element, hit_mask);
2043   }
2044
2045   return TRUE;
2046 }
2047
2048 static boolean HitBlock(int element, int hit_mask)
2049 {
2050   boolean check = FALSE;
2051
2052   if ((element == EL_GATE_STONE || element == EL_GATE_WOOD) &&
2053       game_mm.num_keys == 0)
2054     check = TRUE;
2055
2056   if (element == EL_BLOCK_STONE || element == EL_BLOCK_WOOD)
2057   {
2058     int i, x, y;
2059     int ex = ELX * TILEX + 14;
2060     int ey = ELY * TILEY + 14;
2061
2062     check = TRUE;
2063
2064     for (i = 1; i < 32; i++)
2065     {
2066       x = LX + i * XS;
2067       y = LY + i * YS;
2068
2069       if ((x == ex || x == ex + 1) && (y == ey || y == ey + 1))
2070         check = FALSE;
2071     }
2072   }
2073
2074   if (check && (element == EL_BLOCK_WOOD || element == EL_GATE_WOOD))
2075     return HitAbsorbingWalls(element, hit_mask);
2076
2077   if (check)
2078   {
2079     AddLaserEdge(LX - XS, LY - YS);
2080     AddDamagedField(ELX, ELY);
2081
2082     if (!Box[ELX][ELY])
2083       Box[ELX][ELY] = laser.num_edges;
2084
2085     return HitReflectingWalls(element, hit_mask);
2086   }
2087
2088   if (element == EL_GATE_STONE || element == EL_GATE_WOOD)
2089   {
2090     int xs = XS / 2, ys = YS / 2;
2091
2092     if ((hit_mask & HIT_MASK_DIAGONAL_1) == HIT_MASK_DIAGONAL_1 ||
2093         (hit_mask & HIT_MASK_DIAGONAL_2) == HIT_MASK_DIAGONAL_2)
2094     {
2095       laser.overloaded = (element == EL_GATE_STONE);
2096
2097       return TRUE;
2098     }
2099
2100     if (ABS(xs) == 1 && ABS(ys) == 1 &&
2101         (hit_mask == HIT_MASK_TOP ||
2102          hit_mask == HIT_MASK_LEFT ||
2103          hit_mask == HIT_MASK_RIGHT ||
2104          hit_mask == HIT_MASK_BOTTOM))
2105       AddDamagedField(ELX - xs * (hit_mask == HIT_MASK_TOP ||
2106                                   hit_mask == HIT_MASK_BOTTOM),
2107                       ELY - ys * (hit_mask == HIT_MASK_LEFT ||
2108                                   hit_mask == HIT_MASK_RIGHT));
2109     AddLaserEdge(LX, LY);
2110
2111     Bang_MM(ELX, ELY);
2112
2113     game_mm.num_keys--;
2114
2115     if (element == EL_GATE_STONE && Box[ELX][ELY])
2116     {
2117       DrawLaser(Box[ELX][ELY] - 1, DL_LASER_DISABLED);
2118       /*
2119       BackToFront();
2120       */
2121       ScanLaser();
2122
2123       return TRUE;
2124     }
2125
2126     return FALSE;
2127   }
2128
2129   if (element == EL_BLOCK_STONE || element == EL_BLOCK_WOOD)
2130   {
2131     int xs = XS / 2, ys = YS / 2;
2132
2133     if ((hit_mask & HIT_MASK_DIAGONAL_1) == HIT_MASK_DIAGONAL_1 ||
2134         (hit_mask & HIT_MASK_DIAGONAL_2) == HIT_MASK_DIAGONAL_2)
2135     {
2136       laser.overloaded = (element == EL_BLOCK_STONE);
2137
2138       return TRUE;
2139     }
2140
2141     if (ABS(xs) == 1 && ABS(ys) == 1 &&
2142         (hit_mask == HIT_MASK_TOP ||
2143          hit_mask == HIT_MASK_LEFT ||
2144          hit_mask == HIT_MASK_RIGHT ||
2145          hit_mask == HIT_MASK_BOTTOM))
2146       AddDamagedField(ELX - xs * (hit_mask == HIT_MASK_TOP ||
2147                                   hit_mask == HIT_MASK_BOTTOM),
2148                       ELY - ys * (hit_mask == HIT_MASK_LEFT ||
2149                                   hit_mask == HIT_MASK_RIGHT));
2150     AddDamagedField(ELX, ELY);
2151
2152     LX = ELX * TILEX + 14;
2153     LY = ELY * TILEY + 14;
2154
2155     AddLaserEdge(LX, LY);
2156
2157     laser.stops_inside_element = TRUE;
2158
2159     return TRUE;
2160   }
2161
2162   return TRUE;
2163 }
2164
2165 static boolean HitLaserSource(int element, int hit_mask)
2166 {
2167   if (HitOnlyAnEdge(hit_mask))
2168     return FALSE;
2169
2170   PlayLevelSound_MM(ELX, ELY, element, MM_ACTION_HITTING);
2171
2172   laser.overloaded = TRUE;
2173
2174   return TRUE;
2175 }
2176
2177 static boolean HitLaserDestination(int element, int hit_mask)
2178 {
2179   if (HitOnlyAnEdge(hit_mask))
2180     return FALSE;
2181
2182   if (element != EL_EXIT_OPEN &&
2183       !(IS_RECEIVER(element) &&
2184         game_mm.kettles_still_needed == 0 &&
2185         laser.current_angle == get_opposite_angle(get_element_angle(element))))
2186   {
2187     PlayLevelSound_MM(ELX, ELY, element, MM_ACTION_HITTING);
2188
2189     return TRUE;
2190   }
2191
2192   if (IS_RECEIVER(element) ||
2193       (IS_22_5_ANGLE(laser.current_angle) &&
2194        (ELX != (LX + 6 * XS) / TILEX ||
2195         ELY != (LY + 6 * YS) / TILEY ||
2196         LX + 6 * XS < 0 ||
2197         LY + 6 * YS < 0)))
2198   {
2199     LX -= XS;
2200     LY -= YS;
2201   }
2202   else
2203   {
2204     LX = ELX * TILEX + 14;
2205     LY = ELY * TILEY + 14;
2206
2207     laser.stops_inside_element = TRUE;
2208   }
2209
2210   AddLaserEdge(LX, LY);
2211   AddDamagedField(ELX, ELY);
2212
2213   if (game_mm.lights_still_needed == 0)
2214   {
2215     game_mm.level_solved = TRUE;
2216
2217     SetTileCursorActive(FALSE);
2218   }
2219
2220   return TRUE;
2221 }
2222
2223 static boolean HitReflectingWalls(int element, int hit_mask)
2224 {
2225   // check if laser hits side of a wall with an angle that is not 90°
2226   if (!IS_90_ANGLE(laser.current_angle) && (hit_mask == HIT_MASK_TOP ||
2227                                             hit_mask == HIT_MASK_LEFT ||
2228                                             hit_mask == HIT_MASK_RIGHT ||
2229                                             hit_mask == HIT_MASK_BOTTOM))
2230   {
2231     PlayLevelSound_MM(ELX, ELY, element, MM_ACTION_HITTING);
2232
2233     LX -= XS;
2234     LY -= YS;
2235
2236     if (!IS_DF_GRID(element))
2237       AddLaserEdge(LX, LY);
2238
2239     // check if laser hits wall with an angle of 45°
2240     if (!IS_22_5_ANGLE(laser.current_angle))
2241     {
2242       if (hit_mask == HIT_MASK_TOP || hit_mask == HIT_MASK_BOTTOM)
2243       {
2244         LX += 2 * XS;
2245         laser.current_angle = get_mirrored_angle(laser.current_angle,
2246                                                  ANG_MIRROR_0);
2247       }
2248       else      // hit_mask == HIT_MASK_LEFT || hit_mask == HIT_MASK_RIGHT
2249       {
2250         LY += 2 * YS;
2251         laser.current_angle = get_mirrored_angle(laser.current_angle,
2252                                                  ANG_MIRROR_90);
2253       }
2254
2255       AddLaserEdge(LX, LY);
2256
2257       XS = 2 * Step[laser.current_angle].x;
2258       YS = 2 * Step[laser.current_angle].y;
2259
2260       return FALSE;
2261     }
2262     else if (hit_mask == HIT_MASK_TOP || hit_mask == HIT_MASK_BOTTOM)
2263     {
2264       laser.current_angle = get_mirrored_angle(laser.current_angle,
2265                                                ANG_MIRROR_0);
2266       if (ABS(XS) == 4)
2267       {
2268         LX += 2 * XS;
2269         if (!IS_DF_GRID(element))
2270           AddLaserEdge(LX, LY);
2271       }
2272       else
2273       {
2274         LX += XS;
2275         if (!IS_DF_GRID(element))
2276           AddLaserEdge(LX, LY + YS / 2);
2277
2278         LX += XS;
2279         if (!IS_DF_GRID(element))
2280           AddLaserEdge(LX, LY);
2281       }
2282
2283       YS = 2 * Step[laser.current_angle].y;
2284
2285       return FALSE;
2286     }
2287     else        // hit_mask == HIT_MASK_LEFT || hit_mask == HIT_MASK_RIGHT
2288     {
2289       laser.current_angle = get_mirrored_angle(laser.current_angle,
2290                                                ANG_MIRROR_90);
2291       if (ABS(YS) == 4)
2292       {
2293         LY += 2 * YS;
2294         if (!IS_DF_GRID(element))
2295           AddLaserEdge(LX, LY);
2296       }
2297       else
2298       {
2299         LY += YS;
2300         if (!IS_DF_GRID(element))
2301           AddLaserEdge(LX + XS / 2, LY);
2302
2303         LY += YS;
2304         if (!IS_DF_GRID(element))
2305           AddLaserEdge(LX, LY);
2306       }
2307
2308       XS = 2 * Step[laser.current_angle].x;
2309
2310       return FALSE;
2311     }
2312   }
2313
2314   // reflection at the edge of reflecting DF style wall
2315   if (IS_DF_WALL_STEEL(element) && IS_22_5_ANGLE(laser.current_angle))
2316   {
2317     if (((laser.current_angle == 1 || laser.current_angle == 3) &&
2318          hit_mask == HIT_MASK_TOPRIGHT) ||
2319         ((laser.current_angle == 5 || laser.current_angle == 7) &&
2320          hit_mask == HIT_MASK_TOPLEFT) ||
2321         ((laser.current_angle == 9 || laser.current_angle == 11) &&
2322          hit_mask == HIT_MASK_BOTTOMLEFT) ||
2323         ((laser.current_angle == 13 || laser.current_angle == 15) &&
2324          hit_mask == HIT_MASK_BOTTOMRIGHT))
2325     {
2326       int mirror_angle =
2327         (hit_mask == HIT_MASK_TOPRIGHT || hit_mask == HIT_MASK_BOTTOMLEFT ?
2328          ANG_MIRROR_135 : ANG_MIRROR_45);
2329
2330       PlayLevelSound_MM(ELX, ELY, element, MM_ACTION_HITTING);
2331
2332       AddDamagedField(ELX, ELY);
2333       AddLaserEdge(LX, LY);
2334
2335       laser.current_angle = get_mirrored_angle(laser.current_angle,
2336                                                mirror_angle);
2337       XS = 8 / -XS;
2338       YS = 8 / -YS;
2339
2340       LX += XS;
2341       LY += YS;
2342
2343       AddLaserEdge(LX, LY);
2344
2345       return FALSE;
2346     }
2347   }
2348
2349   // reflection inside an edge of reflecting DF style wall
2350   if (IS_DF_WALL_STEEL(element) && IS_22_5_ANGLE(laser.current_angle))
2351   {
2352     if (((laser.current_angle == 1 || laser.current_angle == 3) &&
2353          hit_mask == (HIT_MASK_ALL ^ HIT_MASK_BOTTOMLEFT)) ||
2354         ((laser.current_angle == 5 || laser.current_angle == 7) &&
2355          hit_mask == (HIT_MASK_ALL ^ HIT_MASK_BOTTOMRIGHT)) ||
2356         ((laser.current_angle == 9 || laser.current_angle == 11) &&
2357          hit_mask == (HIT_MASK_ALL ^ HIT_MASK_TOPRIGHT)) ||
2358         ((laser.current_angle == 13 || laser.current_angle == 15) &&
2359          hit_mask == (HIT_MASK_ALL ^ HIT_MASK_TOPLEFT)))
2360     {
2361       int mirror_angle =
2362         (hit_mask == (HIT_MASK_ALL ^ HIT_MASK_BOTTOMLEFT) ||
2363          hit_mask == (HIT_MASK_ALL ^ HIT_MASK_TOPRIGHT) ?
2364          ANG_MIRROR_135 : ANG_MIRROR_45);
2365
2366       PlayLevelSound_MM(ELX, ELY, element, MM_ACTION_HITTING);
2367
2368       /*
2369       AddDamagedField(ELX, ELY);
2370       */
2371
2372       AddLaserEdge(LX - XS, LY - YS);
2373       AddLaserEdge(LX - XS + (ABS(XS) == 4 ? XS/2 : 0),
2374                    LY - YS + (ABS(YS) == 4 ? YS/2 : 0));
2375
2376       laser.current_angle = get_mirrored_angle(laser.current_angle,
2377                                                mirror_angle);
2378       XS = 8 / -XS;
2379       YS = 8 / -YS;
2380
2381       LX += XS;
2382       LY += YS;
2383
2384       AddLaserEdge(LX, LY);
2385
2386       return FALSE;
2387     }
2388   }
2389
2390   // check if laser hits DF style wall with an angle of 90°
2391   if (IS_DF_WALL(element) && IS_90_ANGLE(laser.current_angle))
2392   {
2393     if ((IS_HORIZ_ANGLE(laser.current_angle) &&
2394          (!(hit_mask & HIT_MASK_TOP) || !(hit_mask & HIT_MASK_BOTTOM))) ||
2395         (IS_VERT_ANGLE(laser.current_angle) &&
2396          (!(hit_mask & HIT_MASK_LEFT) || !(hit_mask & HIT_MASK_RIGHT))))
2397     {
2398       // laser at last step touched nothing or the same side of the wall
2399       if (LX != last_LX || LY != last_LY || hit_mask == last_hit_mask)
2400       {
2401         AddDamagedField(ELX, ELY);
2402
2403         LX += 8 * XS;
2404         LY += 8 * YS;
2405
2406         last_LX = LX;
2407         last_LY = LY;
2408         last_hit_mask = hit_mask;
2409
2410         return FALSE;
2411       }
2412     }
2413   }
2414
2415   if (!HitOnlyAnEdge(hit_mask))
2416   {
2417     laser.overloaded = TRUE;
2418
2419     return TRUE;
2420   }
2421
2422   return FALSE;
2423 }
2424
2425 static boolean HitAbsorbingWalls(int element, int hit_mask)
2426 {
2427   if (HitOnlyAnEdge(hit_mask))
2428     return FALSE;
2429
2430   if (ABS(XS) == 4 &&
2431       (hit_mask == HIT_MASK_LEFT || hit_mask == HIT_MASK_RIGHT))
2432   {
2433     AddLaserEdge(LX - XS, LY - YS);
2434
2435     LX = LX + XS / 2;
2436     LY = LY + YS;
2437   }
2438
2439   if (ABS(YS) == 4 &&
2440       (hit_mask == HIT_MASK_TOP || hit_mask == HIT_MASK_BOTTOM))
2441   {
2442     AddLaserEdge(LX - XS, LY - YS);
2443
2444     LX = LX + XS;
2445     LY = LY + YS / 2;
2446   }
2447
2448   if (IS_WALL_WOOD(element) ||
2449       IS_DF_WALL_WOOD(element) ||
2450       IS_GRID_WOOD(element) ||
2451       IS_GRID_WOOD_FIXED(element) ||
2452       IS_GRID_WOOD_AUTO(element) ||
2453       element == EL_FUSE_ON ||
2454       element == EL_BLOCK_WOOD ||
2455       element == EL_GATE_WOOD)
2456   {
2457     PlayLevelSound_MM(ELX, ELY, element, MM_ACTION_HITTING);
2458
2459     return TRUE;
2460   }
2461
2462   if (IS_WALL_ICE(element))
2463   {
2464     int lx = LX + XS;
2465     int ly = LY + YS;
2466     int mask;
2467
2468     // check if laser hit adjacent edges of two diagonal tiles
2469     if (ELX != lx / TILEX)
2470       lx = LX - XS;
2471     if (ELY != ly / TILEY)
2472       ly = LY - YS;
2473
2474     mask =     lx / MINI_TILEX - ELX * 2 + 1;    // Quadrant (horizontal)
2475     mask <<= ((ly / MINI_TILEY - ELY * 2) > 0 ? 2 : 0);  // || (vertical)
2476
2477     // check if laser hits wall with an angle of 90°
2478     if (IS_90_ANGLE(laser.current_angle))
2479       mask += mask * (2 + IS_HORIZ_ANGLE(laser.current_angle) * 2);
2480
2481     if (mask == 1 || mask == 2 || mask == 4 || mask == 8)
2482     {
2483       int i;
2484
2485       for (i = 0; i < 4; i++)
2486       {
2487         if (mask == (1 << i) && (XS > 0) == (i % 2) && (YS > 0) == (i / 2))
2488           mask = 15 - (8 >> i);
2489         else if (ABS(XS) == 4 &&
2490                  mask == (1 << i) &&
2491                  (XS > 0) == (i % 2) &&
2492                  (YS < 0) == (i / 2))
2493           mask = 3 + (i / 2) * 9;
2494         else if (ABS(YS) == 4 &&
2495                  mask == (1 << i) &&
2496                  (XS < 0) == (i % 2) &&
2497                  (YS > 0) == (i / 2))
2498           mask = 5 + (i % 2) * 5;
2499       }
2500     }
2501
2502     laser.wall_mask = mask;
2503   }
2504   else if (IS_WALL_AMOEBA(element))
2505   {
2506     int elx = (LX - 2 * XS) / TILEX;
2507     int ely = (LY - 2 * YS) / TILEY;
2508     int element2 = Tile[elx][ely];
2509     int mask;
2510
2511     if (element2 != EL_EMPTY && !IS_WALL_AMOEBA(element2))
2512     {
2513       laser.dest_element = EL_EMPTY;
2514
2515       return TRUE;
2516     }
2517
2518     ELX = elx;
2519     ELY = ely;
2520
2521     mask = (LX - 2 * XS) / 16 - ELX * 2 + 1;
2522     mask <<= ((LY - 2 * YS) / 16 - ELY * 2) * 2;
2523
2524     if (IS_90_ANGLE(laser.current_angle))
2525       mask += mask * (2 + IS_HORIZ_ANGLE(laser.current_angle) * 2);
2526
2527     laser.dest_element = element2 | EL_WALL_AMOEBA_BASE;
2528
2529     laser.wall_mask = mask;
2530   }
2531
2532   return TRUE;
2533 }
2534
2535 static void OpenExit(int x, int y)
2536 {
2537   int delay = 6;
2538
2539   if (!MovDelay[x][y])          // next animation frame
2540     MovDelay[x][y] = 4 * delay;
2541
2542   if (MovDelay[x][y])           // wait some time before next frame
2543   {
2544     int phase;
2545
2546     MovDelay[x][y]--;
2547     phase = MovDelay[x][y] / delay;
2548
2549     if (!(MovDelay[x][y] % delay) && IN_SCR_FIELD(x, y))
2550       DrawGraphicAnimation_MM(x, y, IMG_MM_EXIT_OPENING, 3 - phase);
2551
2552     if (!MovDelay[x][y])
2553     {
2554       Tile[x][y] = EL_EXIT_OPEN;
2555       DrawField_MM(x, y);
2556     }
2557   }
2558 }
2559
2560 static void OpenGrayBall(int x, int y)
2561 {
2562   int delay = 2;
2563
2564   if (!MovDelay[x][y])          // next animation frame
2565   {
2566     if (IS_WALL(Store[x][y]))
2567     {
2568       DrawWalls_MM(x, y, Store[x][y]);
2569
2570       // copy wall tile to spare bitmap for "melting" animation
2571       BlitBitmap(drawto_mm, bitmap_db_field, cSX + x * TILEX, cSY + y * TILEY,
2572                  TILEX, TILEY, x * TILEX, y * TILEY);
2573
2574       DrawElement_MM(x, y, EL_GRAY_BALL);
2575     }
2576
2577     MovDelay[x][y] = 50 * delay;
2578   }
2579
2580   if (MovDelay[x][y])           // wait some time before next frame
2581   {
2582     MovDelay[x][y]--;
2583
2584     if (!(MovDelay[x][y] % delay) && IN_SCR_FIELD(x, y))
2585     {
2586       Bitmap *bitmap;
2587       int gx, gy;
2588       int dx = RND(26), dy = RND(26);
2589
2590       if (IS_WALL(Store[x][y]))
2591       {
2592         // copy wall tile from spare bitmap for "melting" animation
2593         bitmap = bitmap_db_field;
2594         gx = x * TILEX;
2595         gy = y * TILEY;
2596       }
2597       else
2598       {
2599         int graphic = el2gfx(Store[x][y]);
2600
2601         getGraphicSource(graphic, 0, &bitmap, &gx, &gy);
2602       }
2603
2604       BlitBitmap(bitmap, drawto_mm, gx + dx, gy + dy, 6, 6,
2605                  cSX + x * TILEX + dx, cSY + y * TILEY + dy);
2606
2607       laser.redraw = TRUE;
2608
2609       MarkTileDirty(x, y);
2610     }
2611
2612     if (!MovDelay[x][y])
2613     {
2614       Tile[x][y] = Store[x][y];
2615       Store[x][y] = Store2[x][y] = 0;
2616       MovDir[x][y] = MovPos[x][y] = MovDelay[x][y] = 0;
2617
2618       InitField(x, y, FALSE);
2619       DrawField_MM(x, y);
2620
2621       ScanLaser_FromLastMirror();
2622     }
2623   }
2624 }
2625
2626 static void OpenEnvelope(int x, int y)
2627 {
2628   int num_frames = 8;           // seven frames plus final empty space
2629
2630   if (!MovDelay[x][y])          // next animation frame
2631     MovDelay[x][y] = num_frames;
2632
2633   if (MovDelay[x][y])           // wait some time before next frame
2634   {
2635     int nr = ENVELOPE_OPENING_NR(Tile[x][y]);
2636
2637     MovDelay[x][y]--;
2638
2639     if (MovDelay[x][y] > 0 && IN_SCR_FIELD(x, y))
2640     {
2641       int graphic = el_act2gfx(EL_ENVELOPE_1 + nr, MM_ACTION_COLLECTING);
2642       int frame = num_frames - MovDelay[x][y] - 1;
2643
2644       DrawGraphicAnimation_MM(x, y, graphic, frame);
2645
2646       laser.redraw = TRUE;
2647     }
2648
2649     if (MovDelay[x][y] == 0)
2650     {
2651       Tile[x][y] = EL_EMPTY;
2652
2653       DrawField_MM(x, y);
2654
2655       ScanLaser();
2656
2657       ShowEnvelope(nr);
2658     }
2659   }
2660 }
2661
2662 static void MeltIce(int x, int y)
2663 {
2664   int frames = 5;
2665   int delay = 5;
2666
2667   if (!MovDelay[x][y])          // next animation frame
2668     MovDelay[x][y] = frames * delay;
2669
2670   if (MovDelay[x][y])           // wait some time before next frame
2671   {
2672     int phase;
2673     int wall_mask = Store2[x][y];
2674     int real_element = Tile[x][y] - EL_WALL_CHANGING_BASE + EL_WALL_ICE_BASE;
2675
2676     MovDelay[x][y]--;
2677     phase = frames - MovDelay[x][y] / delay - 1;
2678
2679     if (!MovDelay[x][y])
2680     {
2681       Tile[x][y] = real_element & (wall_mask ^ 0xFF);
2682       Store[x][y] = Store2[x][y] = 0;
2683
2684       DrawWalls_MM(x, y, Tile[x][y]);
2685
2686       if (Tile[x][y] == EL_WALL_ICE_BASE)
2687         Tile[x][y] = EL_EMPTY;
2688
2689       ScanLaser_FromLastMirror();
2690     }
2691     else if (!(MovDelay[x][y] % delay) && IN_SCR_FIELD(x, y))
2692     {
2693       DrawWallsAnimation_MM(x, y, real_element, phase, wall_mask);
2694
2695       laser.redraw = TRUE;
2696     }
2697   }
2698 }
2699
2700 static void GrowAmoeba(int x, int y)
2701 {
2702   int frames = 5;
2703   int delay = 1;
2704
2705   if (!MovDelay[x][y])          // next animation frame
2706     MovDelay[x][y] = frames * delay;
2707
2708   if (MovDelay[x][y])           // wait some time before next frame
2709   {
2710     int phase;
2711     int wall_mask = Store2[x][y];
2712     int real_element = Tile[x][y] - EL_WALL_CHANGING_BASE + EL_WALL_AMOEBA_BASE;
2713
2714     MovDelay[x][y]--;
2715     phase = MovDelay[x][y] / delay;
2716
2717     if (!MovDelay[x][y])
2718     {
2719       Tile[x][y] = real_element;
2720       Store[x][y] = Store2[x][y] = 0;
2721
2722       DrawWalls_MM(x, y, Tile[x][y]);
2723       DrawLaser(0, DL_LASER_ENABLED);
2724     }
2725     else if (!(MovDelay[x][y] % delay) && IN_SCR_FIELD(x, y))
2726     {
2727       DrawWallsAnimation_MM(x, y, real_element, phase, wall_mask);
2728     }
2729   }
2730 }
2731
2732 static void DrawFieldAnimated_MM(int x, int y)
2733 {
2734   DrawField_MM(x, y);
2735
2736   laser.redraw = TRUE;
2737 }
2738
2739 static void DrawFieldAnimatedIfNeeded_MM(int x, int y)
2740 {
2741   int element = Tile[x][y];
2742   int graphic = el2gfx(element);
2743
2744   if (!getGraphicInfo_NewFrame(x, y, graphic))
2745     return;
2746
2747   DrawField_MM(x, y);
2748
2749   laser.redraw = TRUE;
2750 }
2751
2752 static void DrawFieldTwinkle(int x, int y)
2753 {
2754   if (MovDelay[x][y] != 0)      // wait some time before next frame
2755   {
2756     MovDelay[x][y]--;
2757
2758     DrawField_MM(x, y);
2759
2760     if (MovDelay[x][y] != 0)
2761     {
2762       int graphic = IMG_TWINKLE_WHITE;
2763       int frame = getGraphicAnimationFrame(graphic, 10 - MovDelay[x][y]);
2764
2765       DrawGraphicThruMask_MM(SCREENX(x), SCREENY(y), graphic, frame);
2766     }
2767
2768     laser.redraw = TRUE;
2769   }
2770 }
2771
2772 static void Explode_MM(int x, int y, int phase, int mode)
2773 {
2774   int num_phase = 9, delay = 2;
2775   int last_phase = num_phase * delay;
2776   int half_phase = (num_phase / 2) * delay;
2777   int center_element;
2778
2779   laser.redraw = TRUE;
2780
2781   if (phase == EX_PHASE_START)          // initialize 'Store[][]' field
2782   {
2783     center_element = Tile[x][y];
2784
2785     if (IS_MOVING(x, y) || IS_BLOCKED(x, y))
2786     {
2787       // put moving element to center field (and let it explode there)
2788       center_element = MovingOrBlocked2Element_MM(x, y);
2789       RemoveMovingField_MM(x, y);
2790
2791       Tile[x][y] = center_element;
2792     }
2793
2794     if (center_element != EL_GRAY_BALL_ACTIVE)
2795       Store[x][y] = EL_EMPTY;
2796     Store2[x][y] = center_element;
2797
2798     Tile[x][y] = EL_EXPLODING_OPAQUE;
2799
2800     GfxElement[x][y] = (center_element == EL_BOMB_ACTIVE ? EL_BOMB :
2801                         center_element == EL_GRAY_BALL_ACTIVE ? EL_GRAY_BALL :
2802                         IS_MCDUFFIN(center_element) ? EL_MCDUFFIN :
2803                         center_element);
2804
2805     MovDir[x][y] = MovPos[x][y] = MovDelay[x][y] = 0;
2806
2807     ExplodePhase[x][y] = 1;
2808
2809     return;
2810   }
2811
2812   if (phase == 1)
2813     GfxFrame[x][y] = 0;         // restart explosion animation
2814
2815   ExplodePhase[x][y] = (phase < last_phase ? phase + 1 : 0);
2816
2817   center_element = Store2[x][y];
2818
2819   if (phase == half_phase && Store[x][y] == EL_EMPTY)
2820   {
2821     Tile[x][y] = EL_EXPLODING_TRANSP;
2822
2823     if (x == ELX && y == ELY)
2824       ScanLaser();
2825   }
2826
2827   if (phase == last_phase)
2828   {
2829     if (center_element == EL_BOMB_ACTIVE)
2830     {
2831       DrawLaser(0, DL_LASER_DISABLED);
2832       InitLaser();
2833
2834       Bang_MM(laser.start_edge.x, laser.start_edge.y);
2835
2836       laser.overloaded = FALSE;
2837     }
2838     else if (IS_MCDUFFIN(center_element) || IS_LASER(center_element))
2839     {
2840       GameOver_MM(GAME_OVER_BOMB);
2841     }
2842
2843     Tile[x][y] = Store[x][y];
2844
2845     Store[x][y] = Store2[x][y] = 0;
2846     MovDir[x][y] = MovPos[x][y] = MovDelay[x][y] = 0;
2847
2848     InitField(x, y, FALSE);
2849     DrawField_MM(x, y);
2850
2851     if (center_element == EL_GRAY_BALL_ACTIVE)
2852       ScanLaser_FromLastMirror();
2853   }
2854   else if (!(phase % delay) && IN_SCR_FIELD(SCREENX(x), SCREENY(y)))
2855   {
2856     int graphic = el_act2gfx(GfxElement[x][y], MM_ACTION_EXPLODING);
2857     int frame = getGraphicAnimationFrameXY(graphic, x, y);
2858
2859     DrawGraphicAnimation_MM(x, y, graphic, frame);
2860
2861     MarkTileDirty(x, y);
2862   }
2863 }
2864
2865 static void Bang_MM(int x, int y)
2866 {
2867   int element = Tile[x][y];
2868
2869   if (IS_PACMAN(element))
2870     PlayLevelSound_MM(x, y, element, MM_ACTION_EXPLODING);
2871   else if (element == EL_BOMB_ACTIVE || IS_MCDUFFIN(element))
2872     PlayLevelSound_MM(x, y, element, MM_ACTION_EXPLODING);
2873   else if (element == EL_KEY)
2874     PlayLevelSound_MM(x, y, element, MM_ACTION_EXPLODING);
2875   else
2876     PlayLevelSound_MM(x, y, element, MM_ACTION_EXPLODING);
2877
2878   Explode_MM(x, y, EX_PHASE_START, EX_TYPE_NORMAL);
2879 }
2880
2881 static void TurnRound(int x, int y)
2882 {
2883   static struct
2884   {
2885     int x, y;
2886   } move_xy[] =
2887   {
2888     {  0,  0 },
2889     { -1,  0 },
2890     { +1,  0 },
2891     {  0,  0 },
2892     {  0, -1 },
2893     {  0,  0 }, { 0, 0 }, { 0, 0 },
2894     {  0, +1 }
2895   };
2896   static struct
2897   {
2898     int left, right, back;
2899   } turn[] =
2900   {
2901     { 0,        0,              0        },
2902     { MV_DOWN,  MV_UP,          MV_RIGHT },
2903     { MV_UP,    MV_DOWN,        MV_LEFT  },
2904     { 0,        0,              0        },
2905     { MV_LEFT,  MV_RIGHT,       MV_DOWN  },
2906     { 0,        0,              0        },
2907     { 0,        0,              0        },
2908     { 0,        0,              0        },
2909     { MV_RIGHT, MV_LEFT,        MV_UP    }
2910   };
2911
2912   int element = Tile[x][y];
2913   int old_move_dir = MovDir[x][y];
2914   int right_dir = turn[old_move_dir].right;
2915   int back_dir = turn[old_move_dir].back;
2916   int right_dx = move_xy[right_dir].x, right_dy = move_xy[right_dir].y;
2917   int right_x = x + right_dx, right_y = y + right_dy;
2918
2919   if (element == EL_PACMAN)
2920   {
2921     boolean can_turn_right = FALSE;
2922
2923     if (IN_LEV_FIELD(right_x, right_y) &&
2924         IS_EATABLE4PACMAN(Tile[right_x][right_y]))
2925       can_turn_right = TRUE;
2926
2927     if (can_turn_right)
2928       MovDir[x][y] = right_dir;
2929     else
2930       MovDir[x][y] = back_dir;
2931
2932     MovDelay[x][y] = 0;
2933   }
2934 }
2935
2936 static void StartMoving_MM(int x, int y)
2937 {
2938   int element = Tile[x][y];
2939
2940   if (Stop[x][y])
2941     return;
2942
2943   if (CAN_MOVE(element))
2944   {
2945     int newx, newy;
2946
2947     if (MovDelay[x][y])         // wait some time before next movement
2948     {
2949       MovDelay[x][y]--;
2950
2951       if (MovDelay[x][y])
2952         return;
2953     }
2954
2955     // now make next step
2956
2957     Moving2Blocked(x, y, &newx, &newy); // get next screen position
2958
2959     if (element == EL_PACMAN &&
2960         IN_LEV_FIELD(newx, newy) && IS_EATABLE4PACMAN(Tile[newx][newy]) &&
2961         !ObjHit(newx, newy, HIT_POS_CENTER))
2962     {
2963       Store[newx][newy] = Tile[newx][newy];
2964       Tile[newx][newy] = EL_EMPTY;
2965
2966       DrawField_MM(newx, newy);
2967     }
2968     else if (!IN_LEV_FIELD(newx, newy) || !IS_FREE(newx, newy) ||
2969              ObjHit(newx, newy, HIT_POS_CENTER))
2970     {
2971       // object was running against a wall
2972
2973       TurnRound(x, y);
2974
2975       return;
2976     }
2977
2978     InitMovingField_MM(x, y, MovDir[x][y]);
2979   }
2980
2981   if (MovDir[x][y])
2982     ContinueMoving_MM(x, y);
2983 }
2984
2985 static void ContinueMoving_MM(int x, int y)
2986 {
2987   int element = Tile[x][y];
2988   int direction = MovDir[x][y];
2989   int dx = (direction == MV_LEFT ? -1 : direction == MV_RIGHT ? +1 : 0);
2990   int dy = (direction == MV_UP   ? -1 : direction == MV_DOWN  ? +1 : 0);
2991   int horiz_move = (dx!=0);
2992   int newx = x + dx, newy = y + dy;
2993   int step = (horiz_move ? dx : dy) * TILEX / 8;
2994
2995   MovPos[x][y] += step;
2996
2997   if (ABS(MovPos[x][y]) >= TILEX)       // object reached its destination
2998   {
2999     Tile[x][y] = EL_EMPTY;
3000     Tile[newx][newy] = element;
3001
3002     MovPos[x][y] = MovDir[x][y] = MovDelay[x][y] = 0;
3003     MovDelay[newx][newy] = 0;
3004
3005     if (!CAN_MOVE(element))
3006       MovDir[newx][newy] = 0;
3007
3008     DrawField_MM(x, y);
3009     DrawField_MM(newx, newy);
3010
3011     Stop[newx][newy] = TRUE;
3012
3013     if (element == EL_PACMAN)
3014     {
3015       if (Store[newx][newy] == EL_BOMB)
3016         Bang_MM(newx, newy);
3017
3018       if (IS_WALL_AMOEBA(Store[newx][newy]) &&
3019           (LX + 2 * XS) / TILEX == newx &&
3020           (LY + 2 * YS) / TILEY == newy)
3021       {
3022         laser.num_edges--;
3023         ScanLaser();
3024       }
3025     }
3026   }
3027   else                          // still moving on
3028   {
3029     DrawField_MM(x, y);
3030   }
3031
3032   laser.redraw = TRUE;
3033 }
3034
3035 boolean ClickElement(int x, int y, int button)
3036 {
3037   static DelayCounter click_delay = { CLICK_DELAY };
3038   static boolean new_button = TRUE;
3039   boolean element_clicked = FALSE;
3040   int element;
3041
3042   if (button == -1)
3043   {
3044     // initialize static variables
3045     click_delay.count = 0;
3046     click_delay.value = CLICK_DELAY;
3047     new_button = TRUE;
3048
3049     return FALSE;
3050   }
3051
3052   // do not rotate objects hit by the laser after the game was solved
3053   if (game_mm.level_solved && Hit[x][y])
3054     return FALSE;
3055
3056   if (button == MB_RELEASED)
3057   {
3058     new_button = TRUE;
3059     click_delay.value = CLICK_DELAY;
3060
3061     // release eventually hold auto-rotating mirror
3062     RotateMirror(x, y, MB_RELEASED);
3063
3064     return FALSE;
3065   }
3066
3067   if (!FrameReached(&click_delay) && !new_button)
3068     return FALSE;
3069
3070   if (button == MB_MIDDLEBUTTON)        // middle button has no function
3071     return FALSE;
3072
3073   if (!IN_LEV_FIELD(x, y))
3074     return FALSE;
3075
3076   if (Tile[x][y] == EL_EMPTY)
3077     return FALSE;
3078
3079   element = Tile[x][y];
3080
3081   if (IS_MIRROR(element) ||
3082       IS_BEAMER(element) ||
3083       IS_POLAR(element) ||
3084       IS_POLAR_CROSS(element) ||
3085       IS_DF_MIRROR(element) ||
3086       IS_DF_MIRROR_AUTO(element))
3087   {
3088     RotateMirror(x, y, button);
3089
3090     element_clicked = TRUE;
3091   }
3092   else if (IS_MCDUFFIN(element))
3093   {
3094     boolean has_laser = (x == laser.start_edge.x && y == laser.start_edge.y);
3095
3096     if (has_laser && !laser.fuse_off)
3097       DrawLaser(0, DL_LASER_DISABLED);
3098
3099     element = get_rotated_element(element, BUTTON_ROTATION(button));
3100
3101     Tile[x][y] = element;
3102     DrawField_MM(x, y);
3103
3104     if (has_laser)
3105     {
3106       laser.start_angle = get_element_angle(element);
3107
3108       InitLaser();
3109
3110       if (!laser.fuse_off)
3111         ScanLaser();
3112     }
3113
3114     element_clicked = TRUE;
3115   }
3116   else if (element == EL_FUSE_ON && laser.fuse_off)
3117   {
3118     if (x != laser.fuse_x || y != laser.fuse_y)
3119       return FALSE;
3120
3121     laser.fuse_off = FALSE;
3122     laser.fuse_x = laser.fuse_y = -1;
3123
3124     DrawGraphic_MM(x, y, IMG_MM_FUSE_ACTIVE);
3125     ScanLaser();
3126
3127     element_clicked = TRUE;
3128   }
3129   else if (element == EL_FUSE_ON && !laser.fuse_off && new_button)
3130   {
3131     laser.fuse_off = TRUE;
3132     laser.fuse_x = x;
3133     laser.fuse_y = y;
3134     laser.overloaded = FALSE;
3135
3136     DrawLaser(0, DL_LASER_DISABLED);
3137     DrawGraphic_MM(x, y, IMG_MM_FUSE);
3138
3139     element_clicked = TRUE;
3140   }
3141   else if (element == EL_LIGHTBALL)
3142   {
3143     Bang_MM(x, y);
3144     RaiseScoreElement_MM(element);
3145     DrawLaser(0, DL_LASER_ENABLED);
3146
3147     element_clicked = TRUE;
3148   }
3149   else if (IS_ENVELOPE(element))
3150   {
3151     Tile[x][y] = EL_ENVELOPE_1_OPENING + ENVELOPE_NR(element);
3152
3153     element_clicked = TRUE;
3154   }
3155
3156   click_delay.value = (new_button ? CLICK_DELAY_FIRST : CLICK_DELAY);
3157   new_button = FALSE;
3158
3159   return element_clicked;
3160 }
3161
3162 static void RotateMirror(int x, int y, int button)
3163 {
3164   if (button == MB_RELEASED)
3165   {
3166     // release eventually hold auto-rotating mirror
3167     hold_x = -1;
3168     hold_y = -1;
3169
3170     return;
3171   }
3172
3173   if (IS_MIRROR(Tile[x][y]) ||
3174       IS_POLAR_CROSS(Tile[x][y]) ||
3175       IS_POLAR(Tile[x][y]) ||
3176       IS_BEAMER(Tile[x][y]) ||
3177       IS_DF_MIRROR(Tile[x][y]) ||
3178       IS_GRID_STEEL_AUTO(Tile[x][y]) ||
3179       IS_GRID_WOOD_AUTO(Tile[x][y]))
3180   {
3181     Tile[x][y] = get_rotated_element(Tile[x][y], BUTTON_ROTATION(button));
3182   }
3183   else if (IS_DF_MIRROR_AUTO(Tile[x][y]))
3184   {
3185     if (button == MB_LEFTBUTTON)
3186     {
3187       // left mouse button only for manual adjustment, no auto-rotating;
3188       // freeze mirror for until mouse button released
3189       hold_x = x;
3190       hold_y = y;
3191     }
3192     else if (button == MB_RIGHTBUTTON && (hold_x != x || hold_y != y))
3193     {
3194       Tile[x][y] = get_rotated_element(Tile[x][y], ROTATE_RIGHT);
3195     }
3196   }
3197
3198   if (IS_GRID_STEEL_AUTO(Tile[x][y]) || IS_GRID_WOOD_AUTO(Tile[x][y]))
3199   {
3200     int edge = Hit[x][y];
3201
3202     DrawField_MM(x, y);
3203
3204     if (edge > 0)
3205     {
3206       DrawLaser(edge - 1, DL_LASER_DISABLED);
3207       ScanLaser();
3208     }
3209   }
3210   else if (ObjHit(x, y, HIT_POS_CENTER))
3211   {
3212     int edge = Hit[x][y];
3213
3214     if (edge == 0)
3215     {
3216       Warn("RotateMirror: inconsistent field Hit[][]!\n");
3217
3218       edge = 1;
3219     }
3220
3221     DrawLaser(edge - 1, DL_LASER_DISABLED);
3222     ScanLaser();
3223   }
3224   else
3225   {
3226     int check = 1;
3227
3228     if (ObjHit(x, y, HIT_POS_EDGE | HIT_POS_BETWEEN))
3229       check = 2;
3230
3231     DrawField_MM(x, y);
3232
3233     if ((IS_BEAMER(Tile[x][y]) ||
3234          IS_POLAR(Tile[x][y]) ||
3235          IS_POLAR_CROSS(Tile[x][y])) && x == ELX && y == ELY)
3236     {
3237       if (IS_BEAMER(Tile[x][y]))
3238       {
3239 #if 0
3240         Debug("game:mm:RotateMirror", "TEST (%d, %d) [%d] [%d]",
3241               LX, LY, laser.beamer_edge, laser.beamer[1].num);
3242 #endif
3243
3244         if (check == 1)
3245           laser.num_edges--;
3246       }
3247
3248       ScanLaser();
3249
3250       check = 0;
3251     }
3252
3253     if (check == 2)
3254       DrawLaser(0, DL_LASER_ENABLED);
3255   }
3256 }
3257
3258 static void AutoRotateMirrors(void)
3259 {
3260   int x, y;
3261
3262   if (!FrameReached(&rotate_delay))
3263     return;
3264
3265   for (x = 0; x < lev_fieldx; x++)
3266   {
3267     for (y = 0; y < lev_fieldy; y++)
3268     {
3269       int element = Tile[x][y];
3270
3271       // do not rotate objects hit by the laser after the game was solved
3272       if (game_mm.level_solved && Hit[x][y])
3273         continue;
3274
3275       if (IS_DF_MIRROR_AUTO(element) ||
3276           IS_GRID_WOOD_AUTO(element) ||
3277           IS_GRID_STEEL_AUTO(element) ||
3278           element == EL_REFRACTOR)
3279         RotateMirror(x, y, MB_RIGHTBUTTON);
3280     }
3281   }
3282 }
3283
3284 static boolean ObjHit(int obx, int oby, int bits)
3285 {
3286   int i;
3287
3288   obx *= TILEX;
3289   oby *= TILEY;
3290
3291   if (bits & HIT_POS_CENTER)
3292   {
3293     if (CheckLaserPixel(cSX + obx + 15,
3294                         cSY + oby + 15))
3295       return TRUE;
3296   }
3297
3298   if (bits & HIT_POS_EDGE)
3299   {
3300     for (i = 0; i < 4; i++)
3301       if (CheckLaserPixel(cSX + obx + 31 * (i % 2),
3302                           cSY + oby + 31 * (i / 2)))
3303         return TRUE;
3304   }
3305
3306   if (bits & HIT_POS_BETWEEN)
3307   {
3308     for (i = 0; i < 4; i++)
3309       if (CheckLaserPixel(cSX + 4 + obx + 22 * (i % 2),
3310                           cSY + 4 + oby + 22 * (i / 2)))
3311         return TRUE;
3312   }
3313
3314   return FALSE;
3315 }
3316
3317 static void DeletePacMan(int px, int py)
3318 {
3319   int i, j;
3320
3321   Bang_MM(px, py);
3322
3323   if (game_mm.num_pacman <= 1)
3324   {
3325     game_mm.num_pacman = 0;
3326     return;
3327   }
3328
3329   for (i = 0; i < game_mm.num_pacman; i++)
3330     if (game_mm.pacman[i].x == px && game_mm.pacman[i].y == py)
3331       break;
3332
3333   game_mm.num_pacman--;
3334
3335   for (j = i; j < game_mm.num_pacman; j++)
3336   {
3337     game_mm.pacman[j].x   = game_mm.pacman[j + 1].x;
3338     game_mm.pacman[j].y   = game_mm.pacman[j + 1].y;
3339     game_mm.pacman[j].dir = game_mm.pacman[j + 1].dir;
3340   }
3341 }
3342
3343 static void GameActions_MM_Ext(void)
3344 {
3345   int element;
3346   int x, y, i;
3347
3348   int r, d;
3349
3350   for (y = 0; y < lev_fieldy; y++) for (x = 0; x < lev_fieldx; x++)
3351     Stop[x][y] = FALSE;
3352
3353   for (y = 0; y < lev_fieldy; y++) for (x = 0; x < lev_fieldx; x++)
3354   {
3355     element = Tile[x][y];
3356
3357     if (!IS_MOVING(x, y) && CAN_MOVE(element))
3358       StartMoving_MM(x, y);
3359     else if (IS_MOVING(x, y))
3360       ContinueMoving_MM(x, y);
3361     else if (IS_EXPLODING(element))
3362       Explode_MM(x, y, ExplodePhase[x][y], EX_TYPE_NORMAL);
3363     else if (element == EL_EXIT_OPENING)
3364       OpenExit(x, y);
3365     else if (element == EL_GRAY_BALL_OPENING)
3366       OpenGrayBall(x, y);
3367     else if (IS_ENVELOPE_OPENING(element))
3368       OpenEnvelope(x, y);
3369     else if (IS_WALL_CHANGING(element) && Store[x][y] == EL_WALL_ICE_BASE)
3370       MeltIce(x, y);
3371     else if (IS_WALL_CHANGING(element) && Store[x][y] == EL_WALL_AMOEBA_BASE)
3372       GrowAmoeba(x, y);
3373     else if (IS_MIRROR(element) ||
3374              IS_MIRROR_FIXED(element) ||
3375              element == EL_PRISM)
3376       DrawFieldTwinkle(x, y);
3377     else if (element == EL_GRAY_BALL_ACTIVE ||
3378              element == EL_BOMB_ACTIVE ||
3379              element == EL_MINE_ACTIVE)
3380       DrawFieldAnimated_MM(x, y);
3381     else if (!IS_BLOCKED(x, y))
3382       DrawFieldAnimatedIfNeeded_MM(x, y);
3383   }
3384
3385   AutoRotateMirrors();
3386
3387 #if 1
3388   // !!! CHANGE THIS: REDRAW ONLY WHEN NEEDED !!!
3389
3390   // redraw after Explode_MM() ...
3391   if (laser.redraw)
3392     DrawLaser(0, DL_LASER_ENABLED);
3393   laser.redraw = FALSE;
3394 #endif
3395
3396   CT = FrameCounter;
3397
3398   if (game_mm.num_pacman && FrameReached(&pacman_delay))
3399   {
3400     MovePacMen();
3401
3402     if (laser.num_damages > MAX_LASER_LEN && !laser.fuse_off)
3403     {
3404       DrawLaser(0, DL_LASER_DISABLED);
3405       ScanLaser();
3406     }
3407   }
3408
3409   // skip all following game actions if game is over
3410   if (game_mm.game_over)
3411     return;
3412
3413   if (game_mm.energy_left == 0 && !game.no_level_time_limit && game.time_limit)
3414   {
3415     FadeOutLaser();
3416
3417     GameOver_MM(GAME_OVER_NO_ENERGY);
3418
3419     return;
3420   }
3421
3422   if (FrameReached(&energy_delay))
3423   {
3424     if (game_mm.energy_left > 0)
3425       game_mm.energy_left--;
3426
3427     // when out of energy, wait another frame to play "out of time" sound
3428   }
3429
3430   element = laser.dest_element;
3431
3432 #if 0
3433   if (element != Tile[ELX][ELY])
3434   {
3435     Debug("game:mm:GameActions_MM_Ext", "element == %d, Tile[ELX][ELY] == %d",
3436           element, Tile[ELX][ELY]);
3437   }
3438 #endif
3439
3440   if (!laser.overloaded && laser.overload_value == 0 &&
3441       element != EL_BOMB &&
3442       element != EL_BOMB_ACTIVE &&
3443       element != EL_MINE &&
3444       element != EL_MINE_ACTIVE &&
3445       element != EL_GRAY_BALL &&
3446       element != EL_GRAY_BALL_ACTIVE &&
3447       element != EL_BLOCK_STONE &&
3448       element != EL_BLOCK_WOOD &&
3449       element != EL_FUSE_ON &&
3450       element != EL_FUEL_FULL &&
3451       !IS_WALL_ICE(element) &&
3452       !IS_WALL_AMOEBA(element))
3453     return;
3454
3455   overload_delay.value = HEALTH_DELAY(laser.overloaded);
3456
3457   if (((laser.overloaded && laser.overload_value < MAX_LASER_OVERLOAD) ||
3458        (!laser.overloaded && laser.overload_value > 0)) &&
3459       FrameReached(&overload_delay))
3460   {
3461     if (laser.overloaded)
3462       laser.overload_value++;
3463     else
3464       laser.overload_value--;
3465
3466     if (game_mm.cheat_no_overload)
3467     {
3468       laser.overloaded = FALSE;
3469       laser.overload_value = 0;
3470     }
3471
3472     game_mm.laser_overload_value = laser.overload_value;
3473
3474     if (laser.overload_value < MAX_LASER_OVERLOAD - 8)
3475     {
3476       SetLaserColor(0xFF);
3477
3478       DrawLaser(0, DL_LASER_ENABLED);
3479     }
3480
3481     if (!laser.overloaded)
3482       StopSound_MM(SND_MM_GAME_HEALTH_CHARGING);
3483     else if (setup.sound_loops)
3484       PlaySoundLoop_MM(SND_MM_GAME_HEALTH_CHARGING);
3485     else
3486       PlaySound_MM(SND_MM_GAME_HEALTH_CHARGING);
3487
3488     if (laser.overload_value == MAX_LASER_OVERLOAD)
3489     {
3490       UpdateAndDisplayGameControlValues();
3491
3492       FadeOutLaser();
3493
3494       GameOver_MM(GAME_OVER_OVERLOADED);
3495
3496       return;
3497     }
3498   }
3499
3500   if (laser.fuse_off)
3501     return;
3502
3503   CT -= Ct;
3504
3505   if (element == EL_BOMB && CT > native_mm_level.time_bomb)
3506   {
3507     if (game_mm.cheat_no_explosion)
3508       return;
3509
3510     Bang_MM(ELX, ELY);
3511
3512     laser.dest_element = EL_EXPLODING_OPAQUE;
3513
3514     return;
3515   }
3516
3517   if (element == EL_FUSE_ON && CT > native_mm_level.time_fuse)
3518   {
3519     laser.fuse_off = TRUE;
3520     laser.fuse_x = ELX;
3521     laser.fuse_y = ELY;
3522
3523     DrawLaser(0, DL_LASER_DISABLED);
3524     DrawGraphic_MM(ELX, ELY, IMG_MM_FUSE);
3525   }
3526
3527   if (element == EL_GRAY_BALL && CT > native_mm_level.time_ball)
3528   {
3529     if (!Store2[ELX][ELY])      // check if content element not yet determined
3530     {
3531       int last_anim_random_frame = gfx.anim_random_frame;
3532       int element_pos;
3533
3534       if (native_mm_level.ball_choice_mode == ANIM_RANDOM)
3535         gfx.anim_random_frame = RND(native_mm_level.num_ball_contents);
3536
3537       element_pos = getAnimationFrame(native_mm_level.num_ball_contents, 1,
3538                                       native_mm_level.ball_choice_mode, 0,
3539                                       game_mm.ball_choice_pos);
3540
3541       if (native_mm_level.ball_choice_mode == ANIM_RANDOM)
3542         gfx.anim_random_frame = last_anim_random_frame;
3543
3544       game_mm.ball_choice_pos++;
3545
3546       int new_element = native_mm_level.ball_content[element_pos];
3547       int new_element_base = map_wall_to_base_element(new_element);
3548
3549       if (IS_WALL(new_element_base))
3550       {
3551         // always use completely filled wall element
3552         new_element = new_element_base | 0x000f;
3553       }
3554       else if (native_mm_level.rotate_ball_content &&
3555                get_num_elements(new_element) > 1)
3556       {
3557         // randomly rotate newly created game element
3558         new_element = get_rotated_element(new_element, RND(16));
3559       }
3560
3561       Store[ELX][ELY] = new_element;
3562       Store2[ELX][ELY] = TRUE;
3563     }
3564
3565     if (native_mm_level.explode_ball)
3566       Bang_MM(ELX, ELY);
3567     else
3568       Tile[ELX][ELY] = EL_GRAY_BALL_OPENING;
3569
3570     laser.dest_element = laser.dest_element_last = Tile[ELX][ELY];
3571
3572     return;
3573   }
3574
3575   if (IS_WALL_ICE(element) && CT > 50)
3576   {
3577     PlayLevelSound_MM(ELX, ELY, element, MM_ACTION_SHRINKING);
3578
3579     Tile[ELX][ELY] = Tile[ELX][ELY] - EL_WALL_ICE_BASE + EL_WALL_CHANGING_BASE;
3580     Store[ELX][ELY] = EL_WALL_ICE_BASE;
3581     Store2[ELX][ELY] = laser.wall_mask;
3582
3583     laser.dest_element = Tile[ELX][ELY];
3584
3585     return;
3586   }
3587
3588   if (IS_WALL_AMOEBA(element) && CT > 60)
3589   {
3590     int k1, k2, k3;
3591     int element2 = Tile[ELX][ELY];
3592
3593     if (element2 != EL_EMPTY && !IS_WALL_AMOEBA(element))
3594       return;
3595
3596     for (i = laser.num_damages - 1; i >= 0; i--)
3597       if (laser.damage[i].is_mirror)
3598         break;
3599
3600     r = laser.num_edges;
3601     d = laser.num_damages;
3602     k1 = i;
3603
3604     if (k1 > 0)
3605     {
3606       int x, y;
3607
3608       DrawLaser(laser.damage[k1].edge - 1, DL_LASER_DISABLED);
3609
3610       laser.num_edges++;
3611       DrawLaser(0, DL_LASER_ENABLED);
3612       laser.num_edges--;
3613
3614       x = laser.damage[k1].x;
3615       y = laser.damage[k1].y;
3616
3617       DrawField_MM(x, y);
3618     }
3619
3620     for (i = 0; i < 4; i++)
3621     {
3622       if (laser.wall_mask & (1 << i))
3623       {
3624         if (CheckLaserPixel(cSX + ELX * TILEX + 14 + (i % 2) * 2,
3625                             cSY + ELY * TILEY + 31 * (i / 2)))
3626           break;
3627
3628         if (CheckLaserPixel(cSX + ELX * TILEX + 31 * (i % 2),
3629                             cSY + ELY * TILEY + 14 + (i / 2) * 2))
3630           break;
3631       }
3632     }
3633
3634     k2 = i;
3635
3636     for (i = 0; i < 4; i++)
3637     {
3638       if (laser.wall_mask & (1 << i))
3639       {
3640         if (CheckLaserPixel(cSX + ELX * TILEX + 31 * (i % 2),
3641                             cSY + ELY * TILEY + 31 * (i / 2)))
3642           break;
3643       }
3644     }
3645
3646     k3 = i;
3647
3648     if (laser.num_beamers > 0 ||
3649         k1 < 1 || k2 < 4 || k3 < 4 ||
3650         CheckLaserPixel(cSX + ELX * TILEX + 14,
3651                         cSY + ELY * TILEY + 14))
3652     {
3653       laser.num_edges = r;
3654       laser.num_damages = d;
3655
3656       DrawLaser(0, DL_LASER_DISABLED);
3657     }
3658
3659     Tile[ELX][ELY] = element | laser.wall_mask;
3660
3661     int x = ELX, y = ELY;
3662     int wall_mask = laser.wall_mask;
3663
3664     ScanLaser();
3665     DrawLaser(0, DL_LASER_ENABLED);
3666
3667     PlayLevelSound_MM(x, y, element, MM_ACTION_GROWING);
3668
3669     Tile[x][y] = Tile[x][y] - EL_WALL_AMOEBA_BASE + EL_WALL_CHANGING_BASE;
3670     Store[x][y] = EL_WALL_AMOEBA_BASE;
3671     Store2[x][y] = wall_mask;
3672
3673     return;
3674   }
3675
3676   if ((element == EL_BLOCK_WOOD || element == EL_BLOCK_STONE) &&
3677       laser.stops_inside_element && CT > native_mm_level.time_block)
3678   {
3679     int x, y;
3680     int k;
3681
3682     if (ABS(XS) > ABS(YS))
3683       k = 0;
3684     else
3685       k = 1;
3686     if (XS < YS)
3687       k += 2;
3688
3689     for (i = 0; i < 4; i++)
3690     {
3691       if (i)
3692         k++;
3693       if (k > 3)
3694         k = 0;
3695
3696       x = ELX + Step[k * 4].x;
3697       y = ELY + Step[k * 4].y;
3698
3699       if (!IN_LEV_FIELD(x, y) || Tile[x][y] != EL_EMPTY)
3700         continue;
3701
3702       if (ObjHit(x, y, HIT_POS_CENTER | HIT_POS_EDGE | HIT_POS_BETWEEN))
3703         continue;
3704
3705       break;
3706     }
3707
3708     if (i > 3)
3709     {
3710       laser.overloaded = (element == EL_BLOCK_STONE);
3711
3712       return;
3713     }
3714
3715     PlayLevelSound_MM(ELX, ELY, element, MM_ACTION_PUSHING);
3716
3717     Tile[ELX][ELY] = 0;
3718     Tile[x][y] = element;
3719
3720     DrawGraphic_MM(ELX, ELY, IMG_EMPTY);
3721     DrawField_MM(x, y);
3722
3723     if (element == EL_BLOCK_STONE && Box[ELX][ELY])
3724     {
3725       DrawLaser(Box[ELX][ELY] - 1, DL_LASER_DISABLED);
3726       DrawLaser(laser.num_edges - 1, DL_LASER_ENABLED);
3727     }
3728
3729     ScanLaser();
3730
3731     return;
3732   }
3733
3734   if (element == EL_FUEL_FULL && CT > 10)
3735   {
3736     int num_init_game_frames = INIT_GAME_ACTIONS_DELAY;
3737     int start = num_init_game_frames * game_mm.energy_left / native_mm_level.time;
3738
3739     for (i = start; i <= num_init_game_frames; i++)
3740     {
3741       if (i == num_init_game_frames)
3742         StopSound_MM(SND_MM_GAME_LEVELTIME_CHARGING);
3743       else if (setup.sound_loops)
3744         PlaySoundLoop_MM(SND_MM_GAME_LEVELTIME_CHARGING);
3745       else
3746         PlaySound_MM(SND_MM_GAME_LEVELTIME_CHARGING);
3747
3748       game_mm.energy_left = native_mm_level.time * i / num_init_game_frames;
3749
3750       UpdateAndDisplayGameControlValues();
3751
3752       BackToFront_MM();
3753     }
3754
3755     Tile[ELX][ELY] = laser.dest_element = EL_FUEL_EMPTY;
3756
3757     DrawField_MM(ELX, ELY);
3758
3759     DrawLaser(0, DL_LASER_ENABLED);
3760
3761     return;
3762   }
3763 }
3764
3765 void GameActions_MM(struct MouseActionInfo action)
3766 {
3767   boolean element_clicked = ClickElement(action.lx, action.ly, action.button);
3768   boolean button_released = (action.button == MB_RELEASED);
3769
3770   GameActions_MM_Ext();
3771
3772   CheckSingleStepMode_MM(element_clicked, button_released);
3773 }
3774
3775 static void MovePacMen(void)
3776 {
3777   int mx, my, ox, oy, nx, ny;
3778   int element;
3779   int l;
3780
3781   if (++pacman_nr >= game_mm.num_pacman)
3782     pacman_nr = 0;
3783
3784   game_mm.pacman[pacman_nr].dir--;
3785
3786   for (l = 1; l < 5; l++)
3787   {
3788     game_mm.pacman[pacman_nr].dir++;
3789
3790     if (game_mm.pacman[pacman_nr].dir > 4)
3791       game_mm.pacman[pacman_nr].dir = 1;
3792
3793     if (game_mm.pacman[pacman_nr].dir % 2)
3794     {
3795       mx = 0;
3796       my = game_mm.pacman[pacman_nr].dir - 2;
3797     }
3798     else
3799     {
3800       my = 0;
3801       mx = 3 - game_mm.pacman[pacman_nr].dir;
3802     }
3803
3804     ox = game_mm.pacman[pacman_nr].x;
3805     oy = game_mm.pacman[pacman_nr].y;
3806     nx = ox + mx;
3807     ny = oy + my;
3808     element = Tile[nx][ny];
3809
3810     if (nx < 0 || nx > 15 || ny < 0 || ny > 11)
3811       continue;
3812
3813     if (!IS_EATABLE4PACMAN(element))
3814       continue;
3815
3816     if (ObjHit(nx, ny, HIT_POS_CENTER))
3817       continue;
3818
3819     Tile[ox][oy] = EL_EMPTY;
3820     Tile[nx][ny] =
3821       EL_PACMAN_RIGHT - 1 +
3822       (game_mm.pacman[pacman_nr].dir - 1 +
3823        (game_mm.pacman[pacman_nr].dir % 2) * 2);
3824
3825     game_mm.pacman[pacman_nr].x = nx;
3826     game_mm.pacman[pacman_nr].y = ny;
3827
3828     DrawGraphic_MM(ox, oy, IMG_EMPTY);
3829
3830     if (element != EL_EMPTY)
3831     {
3832       int graphic = el2gfx(Tile[nx][ny]);
3833       Bitmap *bitmap;
3834       int src_x, src_y;
3835       int i;
3836
3837       getGraphicSource(graphic, 0, &bitmap, &src_x, &src_y);
3838
3839       CT = FrameCounter;
3840       ox = cSX + ox * TILEX;
3841       oy = cSY + oy * TILEY;
3842
3843       for (i = 1; i < 33; i += 2)
3844         BlitBitmap(bitmap, window,
3845                    src_x, src_y, TILEX, TILEY,
3846                    ox + i * mx, oy + i * my);
3847       Ct = Ct + FrameCounter - CT;
3848     }
3849
3850     DrawField_MM(nx, ny);
3851     BackToFront_MM();
3852
3853     if (!laser.fuse_off)
3854     {
3855       DrawLaser(0, DL_LASER_ENABLED);
3856
3857       if (ObjHit(nx, ny, HIT_POS_BETWEEN))
3858       {
3859         AddDamagedField(nx, ny);
3860
3861         laser.damage[laser.num_damages - 1].edge = 0;
3862       }
3863     }
3864
3865     if (element == EL_BOMB)
3866       DeletePacMan(nx, ny);
3867
3868     if (IS_WALL_AMOEBA(element) &&
3869         (LX + 2 * XS) / TILEX == nx &&
3870         (LY + 2 * YS) / TILEY == ny)
3871     {
3872       laser.num_edges--;
3873       ScanLaser();
3874     }
3875
3876     break;
3877   }
3878 }
3879
3880 static void InitMovingField_MM(int x, int y, int direction)
3881 {
3882   int newx = x + (direction == MV_LEFT ? -1 : direction == MV_RIGHT ? +1 : 0);
3883   int newy = y + (direction == MV_UP   ? -1 : direction == MV_DOWN  ? +1 : 0);
3884
3885   MovDir[x][y] = direction;
3886   MovDir[newx][newy] = direction;
3887
3888   if (Tile[newx][newy] == EL_EMPTY)
3889     Tile[newx][newy] = EL_BLOCKED;
3890 }
3891
3892 static int MovingOrBlocked2Element_MM(int x, int y)
3893 {
3894   int element = Tile[x][y];
3895
3896   if (element == EL_BLOCKED)
3897   {
3898     int oldx, oldy;
3899
3900     Blocked2Moving(x, y, &oldx, &oldy);
3901
3902     return Tile[oldx][oldy];
3903   }
3904
3905   return element;
3906 }
3907
3908 static void RemoveMovingField_MM(int x, int y)
3909 {
3910   int oldx = x, oldy = y, newx = x, newy = y;
3911
3912   if (Tile[x][y] != EL_BLOCKED && !IS_MOVING(x, y))
3913     return;
3914
3915   if (IS_MOVING(x, y))
3916   {
3917     Moving2Blocked(x, y, &newx, &newy);
3918     if (Tile[newx][newy] != EL_BLOCKED)
3919       return;
3920   }
3921   else if (Tile[x][y] == EL_BLOCKED)
3922   {
3923     Blocked2Moving(x, y, &oldx, &oldy);
3924     if (!IS_MOVING(oldx, oldy))
3925       return;
3926   }
3927
3928   Tile[oldx][oldy] = EL_EMPTY;
3929   Tile[newx][newy] = EL_EMPTY;
3930   MovPos[oldx][oldy] = MovDir[oldx][oldy] = MovDelay[oldx][oldy] = 0;
3931   MovPos[newx][newy] = MovDir[newx][newy] = MovDelay[newx][newy] = 0;
3932
3933   DrawLevelField_MM(oldx, oldy);
3934   DrawLevelField_MM(newx, newy);
3935 }
3936
3937 static void RaiseScore_MM(int value)
3938 {
3939   game_mm.score += value;
3940 }
3941
3942 void RaiseScoreElement_MM(int element)
3943 {
3944   switch (element)
3945   {
3946     case EL_PACMAN:
3947     case EL_PACMAN_RIGHT:
3948     case EL_PACMAN_UP:
3949     case EL_PACMAN_LEFT:
3950     case EL_PACMAN_DOWN:
3951       RaiseScore_MM(native_mm_level.score[SC_PACMAN]);
3952       break;
3953
3954     case EL_KEY:
3955       RaiseScore_MM(native_mm_level.score[SC_KEY]);
3956       break;
3957
3958     case EL_KETTLE:
3959     case EL_CELL:
3960       RaiseScore_MM(native_mm_level.score[SC_COLLECTIBLE]);
3961       break;
3962
3963     case EL_LIGHTBALL:
3964       RaiseScore_MM(native_mm_level.score[SC_LIGHTBALL]);
3965       break;
3966
3967     default:
3968       break;
3969   }
3970 }
3971
3972
3973 // ----------------------------------------------------------------------------
3974 // Mirror Magic game engine snapshot handling functions
3975 // ----------------------------------------------------------------------------
3976
3977 void SaveEngineSnapshotValues_MM(void)
3978 {
3979   int x, y;
3980
3981   engine_snapshot_mm.game_mm = game_mm;
3982   engine_snapshot_mm.laser = laser;
3983
3984   for (x = 0; x < MAX_PLAYFIELD_WIDTH; x++)
3985   {
3986     for (y = 0; y < MAX_PLAYFIELD_HEIGHT; y++)
3987     {
3988       engine_snapshot_mm.Ur[x][y]    = Ur[x][y];
3989       engine_snapshot_mm.Hit[x][y]   = Hit[x][y];
3990       engine_snapshot_mm.Box[x][y]   = Box[x][y];
3991       engine_snapshot_mm.Angle[x][y] = Angle[x][y];
3992     }
3993   }
3994
3995   engine_snapshot_mm.LX = LX;
3996   engine_snapshot_mm.LY = LY;
3997   engine_snapshot_mm.XS = XS;
3998   engine_snapshot_mm.YS = YS;
3999   engine_snapshot_mm.ELX = ELX;
4000   engine_snapshot_mm.ELY = ELY;
4001   engine_snapshot_mm.CT = CT;
4002   engine_snapshot_mm.Ct = Ct;
4003
4004   engine_snapshot_mm.last_LX = last_LX;
4005   engine_snapshot_mm.last_LY = last_LY;
4006   engine_snapshot_mm.last_hit_mask = last_hit_mask;
4007   engine_snapshot_mm.hold_x = hold_x;
4008   engine_snapshot_mm.hold_y = hold_y;
4009   engine_snapshot_mm.pacman_nr = pacman_nr;
4010
4011   engine_snapshot_mm.rotate_delay = rotate_delay;
4012   engine_snapshot_mm.pacman_delay = pacman_delay;
4013   engine_snapshot_mm.energy_delay = energy_delay;
4014   engine_snapshot_mm.overload_delay = overload_delay;
4015 }
4016
4017 void LoadEngineSnapshotValues_MM(void)
4018 {
4019   int x, y;
4020
4021   // stored engine snapshot buffers already restored at this point
4022
4023   game_mm = engine_snapshot_mm.game_mm;
4024   laser   = engine_snapshot_mm.laser;
4025
4026   for (x = 0; x < MAX_PLAYFIELD_WIDTH; x++)
4027   {
4028     for (y = 0; y < MAX_PLAYFIELD_HEIGHT; y++)
4029     {
4030       Ur[x][y]    = engine_snapshot_mm.Ur[x][y];
4031       Hit[x][y]   = engine_snapshot_mm.Hit[x][y];
4032       Box[x][y]   = engine_snapshot_mm.Box[x][y];
4033       Angle[x][y] = engine_snapshot_mm.Angle[x][y];
4034     }
4035   }
4036
4037   LX  = engine_snapshot_mm.LX;
4038   LY  = engine_snapshot_mm.LY;
4039   XS  = engine_snapshot_mm.XS;
4040   YS  = engine_snapshot_mm.YS;
4041   ELX = engine_snapshot_mm.ELX;
4042   ELY = engine_snapshot_mm.ELY;
4043   CT  = engine_snapshot_mm.CT;
4044   Ct  = engine_snapshot_mm.Ct;
4045
4046   last_LX       = engine_snapshot_mm.last_LX;
4047   last_LY       = engine_snapshot_mm.last_LY;
4048   last_hit_mask = engine_snapshot_mm.last_hit_mask;
4049   hold_x        = engine_snapshot_mm.hold_x;
4050   hold_y        = engine_snapshot_mm.hold_y;
4051   pacman_nr     = engine_snapshot_mm.pacman_nr;
4052
4053   rotate_delay   = engine_snapshot_mm.rotate_delay;
4054   pacman_delay   = engine_snapshot_mm.pacman_delay;
4055   energy_delay   = engine_snapshot_mm.energy_delay;
4056   overload_delay = engine_snapshot_mm.overload_delay;
4057
4058   RedrawPlayfield_MM();
4059 }
4060
4061 static int getAngleFromTouchDelta(int dx, int dy,  int base)
4062 {
4063   double pi = 3.141592653;
4064   double rad = atan2((double)-dy, (double)dx);
4065   double rad2 = (rad < 0 ? rad + 2 * pi : rad);
4066   double deg = rad2 * 180.0 / pi;
4067
4068   return (int)(deg * base / 360.0 + 0.5) % base;
4069 }
4070
4071 int getButtonFromTouchPosition(int x, int y, int dst_mx, int dst_my)
4072 {
4073   // calculate start (source) position to be at the middle of the tile
4074   int src_mx = cSX + x * TILESIZE_VAR + TILESIZE_VAR / 2;
4075   int src_my = cSY + y * TILESIZE_VAR + TILESIZE_VAR / 2;
4076   int dx = dst_mx - src_mx;
4077   int dy = dst_my - src_my;
4078   int element;
4079   int base = 16;
4080   int phases = 16;
4081   int angle_old = -1;
4082   int angle_new = -1;
4083   int button = 0;
4084   int i;
4085
4086   if (!IN_LEV_FIELD(x, y))
4087     return 0;
4088
4089   element = Tile[x][y];
4090
4091   if (!IS_MCDUFFIN(element) &&
4092       !IS_MIRROR(element) &&
4093       !IS_BEAMER(element) &&
4094       !IS_POLAR(element) &&
4095       !IS_POLAR_CROSS(element) &&
4096       !IS_DF_MIRROR(element))
4097     return 0;
4098
4099   angle_old = get_element_angle(element);
4100
4101   if (IS_MCDUFFIN(element))
4102   {
4103     angle_new = (dx > 0 && ABS(dy) < ABS(dx) ? ANG_RAY_RIGHT :
4104                  dy < 0 && ABS(dx) < ABS(dy) ? ANG_RAY_UP :
4105                  dx < 0 && ABS(dy) < ABS(dx) ? ANG_RAY_LEFT :
4106                  dy > 0 && ABS(dx) < ABS(dy) ? ANG_RAY_DOWN :
4107                  -1);
4108   }
4109   else if (IS_MIRROR(element) ||
4110            IS_DF_MIRROR(element))
4111   {
4112     for (i = 0; i < laser.num_damages; i++)
4113     {
4114       if (laser.damage[i].x == x &&
4115           laser.damage[i].y == y &&
4116           ObjHit(x, y, HIT_POS_CENTER))
4117       {
4118         angle_old = get_mirrored_angle(laser.damage[i].angle, angle_old);
4119         angle_new = getAngleFromTouchDelta(dx, dy, base) % phases;
4120
4121         break;
4122       }
4123     }
4124   }
4125
4126   if (angle_new == -1)
4127   {
4128     if (IS_MIRROR(element) ||
4129         IS_DF_MIRROR(element) ||
4130         IS_POLAR(element))
4131       base = 32;
4132
4133     if (IS_POLAR_CROSS(element))
4134       phases = 4;
4135
4136     angle_new = getAngleFromTouchDelta(dx, dy, base) % phases;
4137   }
4138
4139   button = (angle_new == angle_old ? 0 :
4140             (angle_new - angle_old + phases) % phases < (phases / 2) ?
4141             MB_LEFTBUTTON : MB_RIGHTBUTTON);
4142
4143   return button;
4144 }