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