fixed small graphical bug with laser overloaded by slope 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     boolean diagonally_adjacent_hit = FALSE;
1213
1214     // handle special case of laser hitting two diagonally adjacent elements
1215     // (with or without a third corner element behind these two elements)
1216     if ((diag_1 || diag_2) && cross_x && cross_y)
1217     {
1218       diagonally_adjacent_hit = TRUE;
1219
1220       // compare the two diagonally adjacent elements
1221       int xoffset = 2;
1222       int yoffset = 2 * (diag_1 ? -1 : +1);
1223       int elx1 = (LX - xoffset) / TILEX;
1224       int ely1 = (LY + yoffset) / TILEY;
1225       int elx2 = (LX + xoffset) / TILEX;
1226       int ely2 = (LY - yoffset) / TILEY;
1227       int e1 = Tile[elx1][ely1];
1228       int e2 = Tile[elx2][ely2];
1229       boolean use_element_1 = FALSE;
1230
1231       if (IS_WALL_ICE(e1) || IS_WALL_ICE(e2))
1232       {
1233         if (IS_WALL_ICE(e1) && IS_WALL_ICE(e2))
1234           use_element_1 = (RND(2) ? TRUE : FALSE);
1235         else if (IS_WALL_ICE(e1))
1236           use_element_1 = TRUE;
1237       }
1238       else if (IS_WALL_AMOEBA(e1) || IS_WALL_AMOEBA(e2))
1239       {
1240         // if both tiles match, we can just select the first one
1241         if (IS_WALL_AMOEBA(e1))
1242           use_element_1 = TRUE;
1243       }
1244       else if (IS_ABSORBING_BLOCK(e1) || IS_ABSORBING_BLOCK(e2))
1245       {
1246         // if both tiles match, we can just select the first one
1247         if (IS_ABSORBING_BLOCK(e1))
1248           use_element_1 = TRUE;
1249       }
1250
1251       ELX = (use_element_1 ? elx1 : elx2);
1252       ELY = (use_element_1 ? ely1 : ely2);
1253     }
1254
1255 #if 0
1256     Debug("game:mm:ScanLaser", "hit_mask (2) == '%x' (%d, %d) (%d, %d)",
1257           hit_mask, LX, LY, ELX, ELY);
1258 #endif
1259
1260     last_element = element;
1261
1262     element = Tile[ELX][ELY];
1263     laser.dest_element = element;
1264
1265 #if 0
1266     Debug("game:mm:ScanLaser",
1267           "Hit element %d at (%d, %d) [%d, %d] [%d, %d] [%d]",
1268           element, ELX, ELY,
1269           LX, LY,
1270           LX % TILEX, LY % TILEY,
1271           hit_mask);
1272 #endif
1273
1274 #if 0
1275     if (!IN_LEV_FIELD(ELX, ELY))
1276       Debug("game:mm:ScanLaser", "WARNING! (1) %d, %d (%d)",
1277             ELX, ELY, element);
1278 #endif
1279
1280     // special case: leaving fixed MM steel grid (upwards) with non-90° angle
1281     if (element == EL_EMPTY &&
1282         IS_GRID_STEEL(last_element) &&
1283         laser.current_angle % 4)                // angle is not 90°
1284       element = last_element;
1285
1286     if (element == EL_EMPTY)
1287     {
1288       if (!HitOnlyAnEdge(hit_mask))
1289         break;
1290     }
1291     else if (element == EL_FUSE_ON)
1292     {
1293       if (HitPolarizer(element, hit_mask))
1294         break;
1295     }
1296     else if (IS_GRID(element) || IS_DF_GRID(element))
1297     {
1298       if (HitPolarizer(element, hit_mask))
1299         break;
1300     }
1301     else if (element == EL_BLOCK_STONE || element == EL_BLOCK_WOOD ||
1302              element == EL_GATE_STONE || element == EL_GATE_WOOD)
1303     {
1304       if (HitBlock(element, hit_mask))
1305       {
1306         rf = 1;
1307
1308         break;
1309       }
1310     }
1311     else if (IS_MCDUFFIN(element))
1312     {
1313       if (HitLaserSource(element, hit_mask))
1314         break;
1315     }
1316     else if ((element >= EL_EXIT_CLOSED && element <= EL_EXIT_OPEN) ||
1317              IS_RECEIVER(element))
1318     {
1319       if (HitLaserDestination(element, hit_mask))
1320         break;
1321     }
1322     else if (IS_WALL(element))
1323     {
1324       if (IS_WALL_STEEL(element) || IS_DF_WALL_STEEL(element))
1325       {
1326         if (HitReflectingWalls(element, hit_mask))
1327           break;
1328       }
1329       else
1330       {
1331         if (HitAbsorbingWalls(element, hit_mask))
1332           break;
1333       }
1334     }
1335     else if (IS_DF_SLOPE(element))
1336     {
1337       if (diagonally_adjacent_hit)
1338       {
1339         laser.overloaded = TRUE;
1340
1341         break;
1342       }
1343
1344       if (hit_mask == HIT_MASK_LEFT ||
1345           hit_mask == HIT_MASK_RIGHT ||
1346           hit_mask == HIT_MASK_TOP ||
1347           hit_mask == HIT_MASK_BOTTOM)
1348       {
1349         if (HitReflectingWalls(element, hit_mask))
1350           break;
1351       }
1352       else
1353       {
1354         if (HitElement(element, hit_mask))
1355           break;
1356       }
1357     }
1358     else
1359     {
1360       if (HitElement(element, hit_mask))
1361         break;
1362     }
1363
1364     if (rf)
1365       DrawLaser(rf - 1, DL_LASER_ENABLED);
1366     rf = laser.num_edges;
1367
1368     if (!IS_DF_WALL_STEEL(element))
1369     {
1370       // only used for scanning DF steel walls; reset for all other elements
1371       last_LX = 0;
1372       last_LY = 0;
1373       last_hit_mask = 0;
1374     }
1375   }
1376
1377 #if 0
1378   if (laser.dest_element != Tile[ELX][ELY])
1379   {
1380     Debug("game:mm:ScanLaser",
1381           "ALARM: laser.dest_element == %d, Tile[ELX][ELY] == %d",
1382           laser.dest_element, Tile[ELX][ELY]);
1383   }
1384 #endif
1385
1386   if (!end && !laser.stops_inside_element && !StepBehind())
1387   {
1388 #if 0
1389     Debug("game:mm:ScanLaser", "Go one step back");
1390 #endif
1391
1392     LX -= XS;
1393     LY -= YS;
1394
1395     AddLaserEdge(LX, LY);
1396   }
1397
1398   if (rf)
1399     DrawLaser(rf - 1, DL_LASER_ENABLED);
1400
1401   Ct = CT = FrameCounter;
1402
1403 #if 0
1404     if (!IN_LEV_FIELD(ELX, ELY))
1405       Debug("game:mm:ScanLaser", "WARNING! (2) %d, %d", ELX, ELY);
1406 #endif
1407 }
1408
1409 static void ScanLaser_FromLastMirror(void)
1410 {
1411   int start_pos = (laser.num_damages > 0 ? laser.num_damages - 1 : 0);
1412   int i;
1413
1414   for (i = start_pos; i >= 0; i--)
1415     if (laser.damage[i].is_mirror)
1416       break;
1417
1418   int start_edge = (i > 0 ? laser.damage[i].edge - 1 : 0);
1419
1420   DrawLaser(start_edge, DL_LASER_DISABLED);
1421
1422   ScanLaser();
1423 }
1424
1425 static void DrawLaserExt(int start_edge, int num_edges, int mode)
1426 {
1427   int element;
1428   int elx, ely;
1429
1430 #if 0
1431   Debug("game:mm:DrawLaserExt", "start_edge, num_edges, mode == %d, %d, %d",
1432         start_edge, num_edges, mode);
1433 #endif
1434
1435   if (start_edge < 0)
1436   {
1437     Warn("DrawLaserExt: start_edge < 0");
1438
1439     return;
1440   }
1441
1442   if (num_edges < 0)
1443   {
1444     Warn("DrawLaserExt: num_edges < 0");
1445
1446     return;
1447   }
1448
1449 #if 0
1450   if (mode == DL_LASER_DISABLED)
1451   {
1452     Debug("game:mm:DrawLaserExt", "Delete laser from edge %d", start_edge);
1453   }
1454 #endif
1455
1456   // now draw the laser to the backbuffer and (if enabled) to the screen
1457   DrawLaserLines(&laser.edge[start_edge], num_edges, mode);
1458
1459   redraw_mask |= REDRAW_FIELD;
1460
1461   if (mode == DL_LASER_ENABLED)
1462     return;
1463
1464   // after the laser was deleted, the "damaged" graphics must be restored
1465   if (laser.num_damages)
1466   {
1467     int damage_start = 0;
1468     int i;
1469
1470     // determine the starting edge, from which graphics need to be restored
1471     if (start_edge > 0)
1472     {
1473       for (i = 0; i < laser.num_damages; i++)
1474       {
1475         if (laser.damage[i].edge == start_edge + 1)
1476         {
1477           damage_start = i;
1478
1479           break;
1480         }
1481       }
1482     }
1483
1484     // restore graphics from this starting edge to the end of damage list
1485     for (i = damage_start; i < laser.num_damages; i++)
1486     {
1487       int lx = laser.damage[i].x;
1488       int ly = laser.damage[i].y;
1489       int element = Tile[lx][ly];
1490
1491       if (Hit[lx][ly] == laser.damage[i].edge)
1492         if (!((IS_BEAMER(element) || IS_FIBRE_OPTIC(element)) &&
1493                i == damage_start))
1494           Hit[lx][ly] = 0;
1495       if (Box[lx][ly] == laser.damage[i].edge)
1496         Box[lx][ly] = 0;
1497
1498       if (IS_DRAWABLE(element))
1499         DrawField_MM(lx, ly);
1500     }
1501
1502     elx = laser.damage[damage_start].x;
1503     ely = laser.damage[damage_start].y;
1504     element = Tile[elx][ely];
1505
1506 #if 0
1507     if (IS_BEAMER(element))
1508     {
1509       int i;
1510
1511       for (i = 0; i < laser.num_beamers; i++)
1512         Debug("game:mm:DrawLaserExt", "-> %d", laser.beamer_edge[i]);
1513
1514       Debug("game:mm:DrawLaserExt", "IS_BEAMER: [%d]: Hit[%d][%d] == %d [%d]",
1515             mode, elx, ely, Hit[elx][ely], start_edge);
1516       Debug("game:mm:DrawLaserExt", "IS_BEAMER: %d / %d",
1517             get_element_angle(element), laser.damage[damage_start].angle);
1518     }
1519 #endif
1520
1521     if ((IS_BEAMER(element) || IS_FIBRE_OPTIC(element)) &&
1522         laser.num_beamers > 0 &&
1523         start_edge == laser.beamer_edge[laser.num_beamers - 1])
1524     {
1525       // element is outgoing beamer
1526       laser.num_damages = damage_start + 1;
1527
1528       if (IS_BEAMER(element))
1529         laser.current_angle = get_element_angle(element);
1530     }
1531     else
1532     {
1533       // element is incoming beamer or other element
1534       laser.num_damages = damage_start;
1535       laser.current_angle = laser.damage[laser.num_damages].angle;
1536     }
1537   }
1538   else
1539   {
1540     // no damages but McDuffin himself (who needs to be redrawn anyway)
1541
1542     elx = laser.start_edge.x;
1543     ely = laser.start_edge.y;
1544     element = Tile[elx][ely];
1545   }
1546
1547   laser.num_edges = start_edge + 1;
1548   if (start_edge == 0)
1549     laser.current_angle = laser.start_angle;
1550
1551   LX = laser.edge[start_edge].x - cSX2;
1552   LY = laser.edge[start_edge].y - cSY2;
1553   XS = 2 * Step[laser.current_angle].x;
1554   YS = 2 * Step[laser.current_angle].y;
1555
1556 #if 0
1557   Debug("game:mm:DrawLaserExt", "Set (LX, LY) to (%d, %d) [%d]",
1558         LX, LY, element);
1559 #endif
1560
1561   if (start_edge > 0)
1562   {
1563     if (IS_BEAMER(element) ||
1564         IS_FIBRE_OPTIC(element) ||
1565         IS_PACMAN(element) ||
1566         IS_POLAR(element) ||
1567         IS_POLAR_CROSS(element) ||
1568         element == EL_FUSE_ON)
1569     {
1570       int step_size;
1571
1572 #if 0
1573       Debug("game:mm:DrawLaserExt", "element == %d", element);
1574 #endif
1575
1576       if (IS_22_5_ANGLE(laser.current_angle))   // neither 90° nor 45° angle
1577         step_size = ((IS_BEAMER(element) || IS_FIBRE_OPTIC(element)) ? 4 : 3);
1578       else
1579         step_size = 8;
1580
1581       if (IS_POLAR(element) || IS_POLAR_CROSS(element) ||
1582           ((IS_BEAMER(element) || IS_FIBRE_OPTIC(element)) &&
1583            (laser.num_beamers == 0 ||
1584             start_edge != laser.beamer_edge[laser.num_beamers - 1])))
1585       {
1586         // element is incoming beamer or other element
1587         step_size = -step_size;
1588         laser.num_edges--;
1589       }
1590
1591 #if 0
1592       if (IS_BEAMER(element))
1593         Debug("game:mm:DrawLaserExt",
1594               "start_edge == %d, laser.beamer_edge == %d",
1595               start_edge, laser.beamer_edge);
1596 #endif
1597
1598       LX += step_size * XS;
1599       LY += step_size * YS;
1600     }
1601     else if (element != EL_EMPTY)
1602     {
1603       LX -= 3 * XS;
1604       LY -= 3 * YS;
1605       laser.num_edges--;
1606     }
1607   }
1608
1609 #if 0
1610   Debug("game:mm:DrawLaserExt", "Finally: (LX, LY) to (%d, %d) [%d]",
1611         LX, LY, element);
1612 #endif
1613 }
1614
1615 void DrawLaser(int start_edge, int mode)
1616 {
1617   // do not draw laser if fuse is off
1618   if (laser.fuse_off && mode == DL_LASER_ENABLED)
1619     return;
1620
1621   if (mode == DL_LASER_DISABLED)
1622     DeactivateLaserTargetElement();
1623
1624   if (laser.num_edges - start_edge < 0)
1625   {
1626     Warn("DrawLaser: laser.num_edges - start_edge < 0");
1627
1628     return;
1629   }
1630
1631   // check if laser is interrupted by beamer element
1632   if (laser.num_beamers > 0 &&
1633       start_edge < laser.beamer_edge[laser.num_beamers - 1])
1634   {
1635     if (mode == DL_LASER_ENABLED)
1636     {
1637       int i;
1638       int tmp_start_edge = start_edge;
1639
1640       // draw laser segments forward from the start to the last beamer
1641       for (i = 0; i < laser.num_beamers; i++)
1642       {
1643         int tmp_num_edges = laser.beamer_edge[i] - tmp_start_edge;
1644
1645         if (tmp_num_edges <= 0)
1646           continue;
1647
1648 #if 0
1649         Debug("game:mm:DrawLaser", "DL_LASER_ENABLED: i==%d: %d, %d",
1650               i, laser.beamer_edge[i], tmp_start_edge);
1651 #endif
1652
1653         DrawLaserExt(tmp_start_edge, tmp_num_edges, DL_LASER_ENABLED);
1654
1655         tmp_start_edge = laser.beamer_edge[i];
1656       }
1657
1658       // draw last segment from last beamer to the end
1659       DrawLaserExt(tmp_start_edge, laser.num_edges - tmp_start_edge,
1660                    DL_LASER_ENABLED);
1661     }
1662     else
1663     {
1664       int i;
1665       int last_num_edges = laser.num_edges;
1666       int num_beamers = laser.num_beamers;
1667
1668       // delete laser segments backward from the end to the first beamer
1669       for (i = num_beamers - 1; i >= 0; i--)
1670       {
1671         int tmp_num_edges = last_num_edges - laser.beamer_edge[i];
1672
1673         if (laser.beamer_edge[i] - start_edge <= 0)
1674           break;
1675
1676         DrawLaserExt(laser.beamer_edge[i], tmp_num_edges, DL_LASER_DISABLED);
1677
1678         last_num_edges = laser.beamer_edge[i];
1679         laser.num_beamers--;
1680       }
1681
1682 #if 0
1683       if (last_num_edges - start_edge <= 0)
1684         Debug("game:mm:DrawLaser", "DL_LASER_DISABLED: %d, %d",
1685               last_num_edges, start_edge);
1686 #endif
1687
1688       // special case when rotating first beamer: delete laser edge on beamer
1689       // (but do not start scanning on previous edge to prevent mirror sound)
1690       if (last_num_edges - start_edge == 1 && start_edge > 0)
1691         DrawLaserLines(&laser.edge[start_edge - 1], 2, DL_LASER_DISABLED);
1692
1693       // delete first segment from start to the first beamer
1694       DrawLaserExt(start_edge, last_num_edges - start_edge, DL_LASER_DISABLED);
1695     }
1696   }
1697   else
1698   {
1699     DrawLaserExt(start_edge, laser.num_edges - start_edge, mode);
1700   }
1701
1702   game_mm.laser_enabled = mode;
1703 }
1704
1705 void DrawLaser_MM(void)
1706 {
1707   DrawLaser(0, game_mm.laser_enabled);
1708 }
1709
1710 static boolean HitElement(int element, int hit_mask)
1711 {
1712   if (IS_DF_SLOPE(element))
1713   {
1714     // check if laser scan has crossed element boundaries (not just mini tiles)
1715     boolean cross_x = (getLevelFromLaserX(LX) != getLevelFromLaserX(LX + 2));
1716     boolean cross_y = (getLevelFromLaserY(LY) != getLevelFromLaserY(LY + 2));
1717
1718     // check if an edge was hit while crossing element borders
1719     if (cross_x && cross_y && get_number_of_bits(hit_mask) == 1)
1720     {
1721       // check both sides of potentially diagonal side of slope
1722       int dx1 = (LX + XS) % TILEX;
1723       int dy1 = (LY + YS) % TILEY;
1724       int dx2 = (LX + XS + 2) % TILEX;
1725       int dy2 = (LY + YS + 2) % TILEY;
1726       int pos = getMaskFromElement(element);
1727
1728       // check if we are entering empty space area after hitting edge
1729       if (mm_masks[pos][dx1 / 2][dy1 / 2] != 'X' &&
1730           mm_masks[pos][dx2 / 2][dy2 / 2] != 'X')
1731       {
1732         // we already know that we hit an edge, but use this function to go on
1733         if (HitOnlyAnEdge(hit_mask))
1734           return FALSE;
1735       }
1736     }
1737
1738     int mirrored_angle = get_mirrored_angle(laser.current_angle,
1739                                             get_element_angle(element));
1740     int opposite_angle = get_opposite_angle(laser.current_angle);
1741
1742     // check if laser is reflected by slope by 180°
1743     if (mirrored_angle == opposite_angle)
1744     {
1745       AddDamagedField(LX / TILEX, LY / TILEY);
1746
1747       laser.overloaded = TRUE;
1748
1749       return TRUE;
1750     }
1751   }
1752   else
1753   {
1754     if (HitOnlyAnEdge(hit_mask))
1755       return FALSE;
1756   }
1757
1758   if (IS_MOVING(ELX, ELY) || IS_BLOCKED(ELX, ELY))
1759     element = MovingOrBlocked2Element_MM(ELX, ELY);
1760
1761 #if 0
1762   Debug("game:mm:HitElement", "(1): element == %d", element);
1763 #endif
1764
1765 #if 0
1766   if ((ELX * TILEX + 14 - LX) * YS == (ELY * TILEY + 14 - LY) * XS)
1767     Debug("game:mm:HitElement", "(%d): EXACT MATCH @ (%d, %d)",
1768           element, ELX, ELY);
1769   else
1770     Debug("game:mm:HitElement", "(%d): FUZZY MATCH @ (%d, %d)",
1771           element, ELX, ELY);
1772 #endif
1773
1774   AddDamagedField(ELX, ELY);
1775
1776   boolean through_center = ((ELX * TILEX + 14 - LX) * YS ==
1777                             (ELY * TILEY + 14 - LY) * XS);
1778
1779   // this is more precise: check if laser would go through the center
1780   if (!IS_DF_SLOPE(element) && !through_center)
1781   {
1782     int skip_count = 0;
1783
1784     // prevent cutting through laser emitter with laser beam
1785     if (IS_LASER(element))
1786       return TRUE;
1787
1788     // skip the whole element before continuing the scan
1789     do
1790     {
1791       LX += XS;
1792       LY += YS;
1793
1794       skip_count++;
1795     }
1796     while (ELX == LX/TILEX && ELY == LY/TILEY && LX > 0 && LY > 0);
1797
1798     if ((LX/TILEX > ELX || LY/TILEY > ELY) && skip_count > 1)
1799     {
1800       /* skipping scan positions to the right and down skips one scan
1801          position too much, because this is only the top left scan position
1802          of totally four scan positions (plus one to the right, one to the
1803          bottom and one to the bottom right) */
1804       /* ... but only roll back scan position if more than one step done */
1805
1806       LX -= XS;
1807       LY -= YS;
1808     }
1809
1810     return FALSE;
1811   }
1812
1813 #if 0
1814   Debug("game:mm:HitElement", "(2): element == %d", element);
1815 #endif
1816
1817   if (LX + 5 * XS < 0 ||
1818       LY + 5 * YS < 0)
1819   {
1820     LX += 2 * XS;
1821     LY += 2 * YS;
1822
1823     return FALSE;
1824   }
1825
1826 #if 0
1827   Debug("game:mm:HitElement", "(3): element == %d", element);
1828 #endif
1829
1830   if (IS_POLAR(element) &&
1831       ((element - EL_POLAR_START) % 2 ||
1832        (element - EL_POLAR_START) / 2 != laser.current_angle % 8))
1833   {
1834     PlayLevelSound_MM(ELX, ELY, element, MM_ACTION_HITTING);
1835
1836     laser.num_damages--;
1837
1838     return TRUE;
1839   }
1840
1841   if (IS_POLAR_CROSS(element) &&
1842       (element - EL_POLAR_CROSS_START) != laser.current_angle % 4)
1843   {
1844     PlayLevelSound_MM(ELX, ELY, element, MM_ACTION_HITTING);
1845
1846     laser.num_damages--;
1847
1848     return TRUE;
1849   }
1850
1851   if (IS_DF_SLOPE(element) && !through_center)
1852   {
1853     int correction = 2;
1854
1855     if (hit_mask == HIT_MASK_ALL)
1856     {
1857       // laser already inside slope -- go back half step
1858       LX -= XS / 2;
1859       LY -= YS / 2;
1860
1861       correction = 1;
1862     }
1863
1864     AddLaserEdge(LX, LY);
1865
1866     LX -= (ABS(XS) < ABS(YS) ? correction * SIGN(XS) : 0);
1867     LY -= (ABS(YS) < ABS(XS) ? correction * SIGN(YS) : 0);
1868   }
1869   else if (!IS_BEAMER(element) &&
1870            !IS_FIBRE_OPTIC(element) &&
1871            !IS_GRID_WOOD(element) &&
1872            element != EL_FUEL_EMPTY)
1873   {
1874 #if 0
1875     if ((ELX * TILEX + 14 - LX) * YS == (ELY * TILEY + 14 - LY) * XS)
1876       Debug("game:mm:HitElement", "EXACT MATCH @ (%d, %d)", ELX, ELY);
1877     else
1878       Debug("game:mm:HitElement", "FUZZY MATCH @ (%d, %d)", ELX, ELY);
1879 #endif
1880
1881     LX = ELX * TILEX + 14;
1882     LY = ELY * TILEY + 14;
1883
1884     AddLaserEdge(LX, LY);
1885   }
1886
1887   if (IS_MIRROR(element) ||
1888       IS_MIRROR_FIXED(element) ||
1889       IS_POLAR(element) ||
1890       IS_POLAR_CROSS(element) ||
1891       IS_DF_MIRROR(element) ||
1892       IS_DF_MIRROR_AUTO(element) ||
1893       IS_DF_MIRROR_FIXED(element) ||
1894       IS_DF_SLOPE(element) ||
1895       element == EL_PRISM ||
1896       element == EL_REFRACTOR)
1897   {
1898     int current_angle = laser.current_angle;
1899     int step_size;
1900
1901     laser.num_damages--;
1902
1903     AddDamagedField(ELX, ELY);
1904
1905     laser.damage[laser.num_damages - 1].is_mirror = TRUE;
1906
1907     if (!Hit[ELX][ELY])
1908       Hit[ELX][ELY] = laser.damage[laser.num_damages - 1].edge;
1909
1910     if (IS_MIRROR(element) ||
1911         IS_MIRROR_FIXED(element) ||
1912         IS_DF_MIRROR(element) ||
1913         IS_DF_MIRROR_AUTO(element) ||
1914         IS_DF_MIRROR_FIXED(element) ||
1915         IS_DF_SLOPE(element))
1916       laser.current_angle = get_mirrored_angle(laser.current_angle,
1917                                                get_element_angle(element));
1918
1919     if (element == EL_PRISM || element == EL_REFRACTOR)
1920       laser.current_angle = RND(16);
1921
1922     XS = 2 * Step[laser.current_angle].x;
1923     YS = 2 * Step[laser.current_angle].y;
1924
1925     if (through_center)
1926     {
1927       // start from center position for all game elements but slope
1928       if (!IS_22_5_ANGLE(laser.current_angle))  // 90° or 45° angle
1929         step_size = 8;
1930       else
1931         step_size = 4;
1932
1933       LX += step_size * XS;
1934       LY += step_size * YS;
1935     }
1936     else
1937     {
1938       // advance laser position until reaching the next tile (slopes)
1939       while (LX / TILEX == ELX && (LX + 2) / TILEX == ELX &&
1940              LY / TILEY == ELY && (LY + 2) / TILEY == ELY)
1941       {
1942         LX += XS;
1943         LY += YS;
1944       }
1945     }
1946
1947     // draw sparkles on mirror
1948     if ((IS_MIRROR(element) ||
1949          IS_MIRROR_FIXED(element) ||
1950          element == EL_PRISM) &&
1951         current_angle != laser.current_angle)
1952     {
1953       MovDelay[ELX][ELY] = 11;          // start animation
1954     }
1955
1956     if ((!IS_POLAR(element) && !IS_POLAR_CROSS(element)) &&
1957         current_angle != laser.current_angle)
1958       PlayLevelSound_MM(ELX, ELY, element, MM_ACTION_HITTING);
1959
1960     laser.overloaded =
1961       (get_opposite_angle(laser.current_angle) ==
1962        laser.damage[laser.num_damages - 1].angle ? TRUE : FALSE);
1963
1964     if (IS_DF_SLOPE(element))
1965     {
1966       // handle special cases for slope element
1967
1968       if (IS_45_ANGLE(laser.current_angle))
1969       {
1970         int elx, ely;
1971
1972         elx = getLevelFromLaserX(LX);
1973         ely = getLevelFromLaserY(LY);
1974
1975         if (IN_LEV_FIELD(elx, ely))
1976         {
1977           int element_next = Tile[elx][ely];
1978
1979           // check if slope is followed by slope with opposite orientation
1980           if (IS_DF_SLOPE(element_next) && ABS(element - element_next) == 2)
1981             laser.overloaded = TRUE;
1982         }
1983
1984         int nr = element - EL_DF_SLOPE_START;
1985         int dx = (nr == 0 ? (XS > 0 ? TILEX - 1 : -1) :
1986                   nr == 1 ? (XS > 0 ? TILEX     :  1) :
1987                   nr == 2 ? (XS > 0 ? TILEX     :  1) :
1988                   nr == 3 ? (XS > 0 ? TILEX - 1 : -1) : 0);
1989         int dy = (nr == 0 ? (YS > 0 ? TILEY - 1 : -1) :
1990                   nr == 1 ? (YS > 0 ? TILEY - 1 : -1) :
1991                   nr == 2 ? (YS > 0 ? TILEY     :  0) :
1992                   nr == 3 ? (YS > 0 ? TILEY     :  0) : 0);
1993
1994         int px = ELX * TILEX + dx;
1995         int py = ELY * TILEY + dy;
1996
1997         dx = px % TILEX;
1998         dy = py % TILEY;
1999
2000         elx = getLevelFromLaserX(px);
2001         ely = getLevelFromLaserY(py);
2002
2003         if (IN_LEV_FIELD(elx, ely))
2004         {
2005           int element_side = Tile[elx][ely];
2006
2007           // check if end of slope is blocked by other element
2008           if (IS_WALL(element_side) || IS_WALL_CHANGING(element_side))
2009           {
2010             int pos = dy / MINI_TILEY * 2 + dx / MINI_TILEX;
2011
2012             if (element & (1 << pos))
2013               laser.overloaded = TRUE;
2014           }
2015           else
2016           {
2017             int pos = getMaskFromElement(element_side);
2018
2019             if (mm_masks[pos][dx / 2][dy / 2] == 'X')
2020               laser.overloaded = TRUE;
2021           }
2022         }
2023       }
2024     }
2025
2026     return (laser.overloaded ? TRUE : FALSE);
2027   }
2028
2029   if (element == EL_FUEL_FULL)
2030   {
2031     laser.stops_inside_element = TRUE;
2032
2033     return TRUE;
2034   }
2035
2036   if (element == EL_BOMB || element == EL_MINE || element == EL_GRAY_BALL)
2037   {
2038     PlayLevelSound_MM(ELX, ELY, element, MM_ACTION_HITTING);
2039
2040     Tile[ELX][ELY] = (element == EL_BOMB ? EL_BOMB_ACTIVE :
2041                       element == EL_MINE ? EL_MINE_ACTIVE :
2042                       EL_GRAY_BALL_ACTIVE);
2043
2044     GfxFrame[ELX][ELY] = 0;             // restart animation
2045
2046     laser.dest_element_last = Tile[ELX][ELY];
2047     laser.dest_element_last_x = ELX;
2048     laser.dest_element_last_y = ELY;
2049
2050     if (element == EL_MINE)
2051       laser.overloaded = TRUE;
2052   }
2053
2054   if (element == EL_KETTLE ||
2055       element == EL_CELL ||
2056       element == EL_KEY ||
2057       element == EL_LIGHTBALL ||
2058       element == EL_PACMAN ||
2059       IS_PACMAN(element) ||
2060       IS_ENVELOPE(element))
2061   {
2062     if (!IS_PACMAN(element) &&
2063         !IS_ENVELOPE(element))
2064       Bang_MM(ELX, ELY);
2065
2066     if (element == EL_PACMAN)
2067       Bang_MM(ELX, ELY);
2068
2069     if (element == EL_KETTLE || element == EL_CELL)
2070     {
2071       if (game_mm.kettles_still_needed > 0)
2072         game_mm.kettles_still_needed--;
2073
2074       game.snapshot.collected_item = TRUE;
2075
2076       if (game_mm.kettles_still_needed == 0)
2077       {
2078         CheckExitMM();
2079
2080         DrawLaser(0, DL_LASER_ENABLED);
2081       }
2082     }
2083     else if (element == EL_KEY)
2084     {
2085       game_mm.num_keys++;
2086     }
2087     else if (IS_PACMAN(element))
2088     {
2089       DeletePacMan(ELX, ELY);
2090     }
2091     else if (IS_ENVELOPE(element))
2092     {
2093       Tile[ELX][ELY] = EL_ENVELOPE_1_OPENING + ENVELOPE_NR(Tile[ELX][ELY]);
2094     }
2095
2096     RaiseScoreElement_MM(element);
2097
2098     return FALSE;
2099   }
2100
2101   if (element == EL_LIGHTBULB_OFF || element == EL_LIGHTBULB_ON)
2102   {
2103     PlayLevelSound_MM(ELX, ELY, element, MM_ACTION_HITTING);
2104
2105     DrawLaser(0, DL_LASER_ENABLED);
2106
2107     if (Tile[ELX][ELY] == EL_LIGHTBULB_OFF)
2108     {
2109       Tile[ELX][ELY] = EL_LIGHTBULB_ON;
2110       game_mm.lights_still_needed--;
2111     }
2112     else
2113     {
2114       Tile[ELX][ELY] = EL_LIGHTBULB_OFF;
2115       game_mm.lights_still_needed++;
2116     }
2117
2118     DrawField_MM(ELX, ELY);
2119     DrawLaser(0, DL_LASER_ENABLED);
2120
2121     /*
2122     BackToFront();
2123     */
2124     laser.stops_inside_element = TRUE;
2125
2126     return TRUE;
2127   }
2128
2129 #if 0
2130   Debug("game:mm:HitElement", "(4): element == %d", element);
2131 #endif
2132
2133   if ((IS_BEAMER(element) || IS_FIBRE_OPTIC(element)) &&
2134       laser.num_beamers < MAX_NUM_BEAMERS &&
2135       laser.beamer[BEAMER_NR(element)][1].num)
2136   {
2137     int beamer_angle = get_element_angle(element);
2138     int beamer_nr = BEAMER_NR(element);
2139     int step_size;
2140
2141 #if 0
2142     Debug("game:mm:HitElement", "(BEAMER): element == %d", element);
2143 #endif
2144
2145     laser.num_damages--;
2146
2147     if (IS_FIBRE_OPTIC(element) ||
2148         laser.current_angle == get_opposite_angle(beamer_angle))
2149     {
2150       int pos;
2151
2152       LX = ELX * TILEX + 14;
2153       LY = ELY * TILEY + 14;
2154
2155       AddLaserEdge(LX, LY);
2156       AddDamagedField(ELX, ELY);
2157
2158       laser.damage[laser.num_damages - 1].is_mirror = TRUE;
2159
2160       if (!Hit[ELX][ELY])
2161         Hit[ELX][ELY] = laser.damage[laser.num_damages - 1].edge;
2162
2163       pos = (ELX == laser.beamer[beamer_nr][0].x &&
2164              ELY == laser.beamer[beamer_nr][0].y ? 1 : 0);
2165       ELX = laser.beamer[beamer_nr][pos].x;
2166       ELY = laser.beamer[beamer_nr][pos].y;
2167       LX = ELX * TILEX + 14;
2168       LY = ELY * TILEY + 14;
2169
2170       if (IS_BEAMER(element))
2171       {
2172         laser.current_angle = get_element_angle(Tile[ELX][ELY]);
2173         XS = 2 * Step[laser.current_angle].x;
2174         YS = 2 * Step[laser.current_angle].y;
2175       }
2176
2177       laser.beamer_edge[laser.num_beamers] = laser.num_edges;
2178
2179       AddLaserEdge(LX, LY);
2180       AddDamagedField(ELX, ELY);
2181
2182       laser.damage[laser.num_damages - 1].is_mirror = TRUE;
2183
2184       if (!Hit[ELX][ELY])
2185         Hit[ELX][ELY] = laser.damage[laser.num_damages - 1].edge;
2186
2187       if (laser.current_angle == (laser.current_angle >> 1) << 1)
2188         step_size = 8;
2189       else
2190         step_size = 4;
2191
2192       LX += step_size * XS;
2193       LY += step_size * YS;
2194
2195       laser.num_beamers++;
2196
2197       return FALSE;
2198     }
2199   }
2200
2201   return TRUE;
2202 }
2203
2204 static boolean HitOnlyAnEdge(int hit_mask)
2205 {
2206   // check if the laser hit only the edge of an element and, if so, go on
2207
2208 #if 0
2209   Debug("game:mm:HitOnlyAnEdge", "LX, LY, hit_mask == %d, %d, %d",
2210         LX, LY, hit_mask);
2211 #endif
2212
2213   if ((hit_mask == HIT_MASK_TOPLEFT ||
2214        hit_mask == HIT_MASK_TOPRIGHT ||
2215        hit_mask == HIT_MASK_BOTTOMLEFT ||
2216        hit_mask == HIT_MASK_BOTTOMRIGHT) &&
2217       laser.current_angle % 4)                  // angle is not 90°
2218   {
2219     int dx, dy;
2220
2221     if (hit_mask == HIT_MASK_TOPLEFT)
2222     {
2223       dx = -1;
2224       dy = -1;
2225     }
2226     else if (hit_mask == HIT_MASK_TOPRIGHT)
2227     {
2228       dx = +1;
2229       dy = -1;
2230     }
2231     else if (hit_mask == HIT_MASK_BOTTOMLEFT)
2232     {
2233       dx = -1;
2234       dy = +1;
2235     }
2236     else // (hit_mask == HIT_MASK_BOTTOMRIGHT)
2237     {
2238       dx = +1;
2239       dy = +1;
2240     }
2241
2242     AddDamagedField((LX + 2 * dx) / TILEX, (LY + 2 * dy) / TILEY);
2243
2244     LX += XS;
2245     LY += YS;
2246
2247 #if 0
2248     Debug("game:mm:HitOnlyAnEdge", "[HitOnlyAnEdge() == TRUE]");
2249 #endif
2250
2251     return TRUE;
2252   }
2253
2254 #if 0
2255   Debug("game:mm:HitOnlyAnEdge", "[HitOnlyAnEdge() == FALSE]");
2256 #endif
2257
2258   return FALSE;
2259 }
2260
2261 static boolean HitPolarizer(int element, int hit_mask)
2262 {
2263   if (HitOnlyAnEdge(hit_mask))
2264     return FALSE;
2265
2266   if (IS_DF_GRID(element))
2267   {
2268     int grid_angle = get_element_angle(element);
2269
2270 #if 0
2271     Debug("game:mm:HitPolarizer", "angle: grid == %d, laser == %d",
2272           grid_angle, laser.current_angle);
2273 #endif
2274
2275     AddLaserEdge(LX, LY);
2276     AddDamagedField(ELX, ELY);
2277
2278     if (!Hit[ELX][ELY])
2279       Hit[ELX][ELY] = laser.damage[laser.num_damages - 1].edge;
2280
2281     if (laser.current_angle == grid_angle ||
2282         laser.current_angle == get_opposite_angle(grid_angle))
2283     {
2284       // skip the whole element before continuing the scan
2285       do
2286       {
2287         LX += XS;
2288         LY += YS;
2289       }
2290       while (ELX == LX/TILEX && ELY == LY/TILEY && LX > 0 && LY > 0);
2291
2292       if (LX/TILEX > ELX || LY/TILEY > ELY)
2293       {
2294         /* skipping scan positions to the right and down skips one scan
2295            position too much, because this is only the top left scan position
2296            of totally four scan positions (plus one to the right, one to the
2297            bottom and one to the bottom right) */
2298
2299         LX -= XS;
2300         LY -= YS;
2301       }
2302
2303       AddLaserEdge(LX, LY);
2304
2305       LX += XS;
2306       LY += YS;
2307
2308 #if 0
2309       Debug("game:mm:HitPolarizer", "LX, LY == %d, %d [%d, %d] [%d, %d]",
2310             LX, LY,
2311             LX / TILEX, LY / TILEY,
2312             LX % TILEX, LY % TILEY);
2313 #endif
2314
2315       return FALSE;
2316     }
2317     else if (IS_GRID_STEEL_FIXED(element) || IS_GRID_STEEL_AUTO(element))
2318     {
2319       return HitReflectingWalls(element, hit_mask);
2320     }
2321     else
2322     {
2323       return HitAbsorbingWalls(element, hit_mask);
2324     }
2325   }
2326   else if (IS_GRID_STEEL(element))
2327   {
2328     // may be required if graphics for steel grid redefined
2329     AddDamagedField(ELX, ELY);
2330
2331     return HitReflectingWalls(element, hit_mask);
2332   }
2333   else  // IS_GRID_WOOD
2334   {
2335     // may be required if graphics for wooden grid redefined
2336     AddDamagedField(ELX, ELY);
2337
2338     return HitAbsorbingWalls(element, hit_mask);
2339   }
2340
2341   return TRUE;
2342 }
2343
2344 static boolean HitBlock(int element, int hit_mask)
2345 {
2346   boolean check = FALSE;
2347
2348   if ((element == EL_GATE_STONE || element == EL_GATE_WOOD) &&
2349       game_mm.num_keys == 0)
2350     check = TRUE;
2351
2352   if (element == EL_BLOCK_STONE || element == EL_BLOCK_WOOD)
2353   {
2354     int i, x, y;
2355     int ex = ELX * TILEX + 14;
2356     int ey = ELY * TILEY + 14;
2357
2358     check = TRUE;
2359
2360     for (i = 1; i < 32; i++)
2361     {
2362       x = LX + i * XS;
2363       y = LY + i * YS;
2364
2365       if ((x == ex || x == ex + 1) && (y == ey || y == ey + 1))
2366         check = FALSE;
2367     }
2368   }
2369
2370   if (check && (element == EL_BLOCK_WOOD || element == EL_GATE_WOOD))
2371     return HitAbsorbingWalls(element, hit_mask);
2372
2373   if (check)
2374   {
2375     AddLaserEdge(LX - XS, LY - YS);
2376     AddDamagedField(ELX, ELY);
2377
2378     if (!Box[ELX][ELY])
2379       Box[ELX][ELY] = laser.num_edges;
2380
2381     return HitReflectingWalls(element, hit_mask);
2382   }
2383
2384   if (element == EL_GATE_STONE || element == EL_GATE_WOOD)
2385   {
2386     int xs = XS / 2, ys = YS / 2;
2387
2388     if ((hit_mask & HIT_MASK_DIAGONAL_1) == HIT_MASK_DIAGONAL_1 ||
2389         (hit_mask & HIT_MASK_DIAGONAL_2) == HIT_MASK_DIAGONAL_2)
2390     {
2391       laser.overloaded = (element == EL_GATE_STONE);
2392
2393       return TRUE;
2394     }
2395
2396     if (ABS(xs) == 1 && ABS(ys) == 1 &&
2397         (hit_mask == HIT_MASK_TOP ||
2398          hit_mask == HIT_MASK_LEFT ||
2399          hit_mask == HIT_MASK_RIGHT ||
2400          hit_mask == HIT_MASK_BOTTOM))
2401       AddDamagedField(ELX - xs * (hit_mask == HIT_MASK_TOP ||
2402                                   hit_mask == HIT_MASK_BOTTOM),
2403                       ELY - ys * (hit_mask == HIT_MASK_LEFT ||
2404                                   hit_mask == HIT_MASK_RIGHT));
2405     AddLaserEdge(LX, LY);
2406
2407     Bang_MM(ELX, ELY);
2408
2409     game_mm.num_keys--;
2410
2411     if (element == EL_GATE_STONE && Box[ELX][ELY])
2412     {
2413       DrawLaser(Box[ELX][ELY] - 1, DL_LASER_DISABLED);
2414       /*
2415       BackToFront();
2416       */
2417       ScanLaser();
2418
2419       return TRUE;
2420     }
2421
2422     return FALSE;
2423   }
2424
2425   if (element == EL_BLOCK_STONE || element == EL_BLOCK_WOOD)
2426   {
2427     int xs = XS / 2, ys = YS / 2;
2428
2429     if ((hit_mask & HIT_MASK_DIAGONAL_1) == HIT_MASK_DIAGONAL_1 ||
2430         (hit_mask & HIT_MASK_DIAGONAL_2) == HIT_MASK_DIAGONAL_2)
2431     {
2432       laser.overloaded = (element == EL_BLOCK_STONE);
2433
2434       return TRUE;
2435     }
2436
2437     if (ABS(xs) == 1 && ABS(ys) == 1 &&
2438         (hit_mask == HIT_MASK_TOP ||
2439          hit_mask == HIT_MASK_LEFT ||
2440          hit_mask == HIT_MASK_RIGHT ||
2441          hit_mask == HIT_MASK_BOTTOM))
2442       AddDamagedField(ELX - xs * (hit_mask == HIT_MASK_TOP ||
2443                                   hit_mask == HIT_MASK_BOTTOM),
2444                       ELY - ys * (hit_mask == HIT_MASK_LEFT ||
2445                                   hit_mask == HIT_MASK_RIGHT));
2446     AddDamagedField(ELX, ELY);
2447
2448     LX = ELX * TILEX + 14;
2449     LY = ELY * TILEY + 14;
2450
2451     AddLaserEdge(LX, LY);
2452
2453     laser.stops_inside_element = TRUE;
2454
2455     return TRUE;
2456   }
2457
2458   return TRUE;
2459 }
2460
2461 static boolean HitLaserSource(int element, int hit_mask)
2462 {
2463   if (HitOnlyAnEdge(hit_mask))
2464     return FALSE;
2465
2466   PlayLevelSound_MM(ELX, ELY, element, MM_ACTION_HITTING);
2467
2468   laser.overloaded = TRUE;
2469
2470   return TRUE;
2471 }
2472
2473 static boolean HitLaserDestination(int element, int hit_mask)
2474 {
2475   if (HitOnlyAnEdge(hit_mask))
2476     return FALSE;
2477
2478   if (element != EL_EXIT_OPEN &&
2479       !(IS_RECEIVER(element) &&
2480         game_mm.kettles_still_needed == 0 &&
2481         laser.current_angle == get_opposite_angle(get_element_angle(element))))
2482   {
2483     PlayLevelSound_MM(ELX, ELY, element, MM_ACTION_HITTING);
2484
2485     return TRUE;
2486   }
2487
2488   if (IS_RECEIVER(element) ||
2489       (IS_22_5_ANGLE(laser.current_angle) &&
2490        (ELX != (LX + 6 * XS) / TILEX ||
2491         ELY != (LY + 6 * YS) / TILEY ||
2492         LX + 6 * XS < 0 ||
2493         LY + 6 * YS < 0)))
2494   {
2495     LX -= XS;
2496     LY -= YS;
2497   }
2498   else
2499   {
2500     LX = ELX * TILEX + 14;
2501     LY = ELY * TILEY + 14;
2502
2503     laser.stops_inside_element = TRUE;
2504   }
2505
2506   AddLaserEdge(LX, LY);
2507   AddDamagedField(ELX, ELY);
2508
2509   if (game_mm.lights_still_needed == 0)
2510   {
2511     game_mm.level_solved = TRUE;
2512
2513     SetTileCursorActive(FALSE);
2514   }
2515
2516   return TRUE;
2517 }
2518
2519 static boolean HitReflectingWalls(int element, int hit_mask)
2520 {
2521   // check if laser hits side of a wall with an angle that is not 90°
2522   if (!IS_90_ANGLE(laser.current_angle) && (hit_mask == HIT_MASK_TOP ||
2523                                             hit_mask == HIT_MASK_LEFT ||
2524                                             hit_mask == HIT_MASK_RIGHT ||
2525                                             hit_mask == HIT_MASK_BOTTOM))
2526   {
2527     PlayLevelSound_MM(ELX, ELY, element, MM_ACTION_HITTING);
2528
2529     LX -= XS;
2530     LY -= YS;
2531
2532     if (!IS_DF_GRID(element))
2533       AddLaserEdge(LX, LY);
2534
2535     // check if laser hits wall with an angle of 45°
2536     if (!IS_22_5_ANGLE(laser.current_angle))
2537     {
2538       if (hit_mask == HIT_MASK_TOP || hit_mask == HIT_MASK_BOTTOM)
2539       {
2540         LX += 2 * XS;
2541         laser.current_angle = get_mirrored_angle(laser.current_angle,
2542                                                  ANG_MIRROR_0);
2543       }
2544       else      // hit_mask == HIT_MASK_LEFT || hit_mask == HIT_MASK_RIGHT
2545       {
2546         LY += 2 * YS;
2547         laser.current_angle = get_mirrored_angle(laser.current_angle,
2548                                                  ANG_MIRROR_90);
2549       }
2550
2551       AddLaserEdge(LX, LY);
2552
2553       XS = 2 * Step[laser.current_angle].x;
2554       YS = 2 * Step[laser.current_angle].y;
2555
2556       return FALSE;
2557     }
2558     else if (hit_mask == HIT_MASK_TOP || hit_mask == HIT_MASK_BOTTOM)
2559     {
2560       laser.current_angle = get_mirrored_angle(laser.current_angle,
2561                                                ANG_MIRROR_0);
2562       if (ABS(XS) == 4)
2563       {
2564         LX += 2 * XS;
2565         if (!IS_DF_GRID(element))
2566           AddLaserEdge(LX, LY);
2567       }
2568       else
2569       {
2570         LX += XS;
2571         if (!IS_DF_GRID(element))
2572           AddLaserEdge(LX, LY + YS / 2);
2573
2574         LX += XS;
2575         if (!IS_DF_GRID(element))
2576           AddLaserEdge(LX, LY);
2577       }
2578
2579       YS = 2 * Step[laser.current_angle].y;
2580
2581       return FALSE;
2582     }
2583     else        // hit_mask == HIT_MASK_LEFT || hit_mask == HIT_MASK_RIGHT
2584     {
2585       laser.current_angle = get_mirrored_angle(laser.current_angle,
2586                                                ANG_MIRROR_90);
2587       if (ABS(YS) == 4)
2588       {
2589         LY += 2 * YS;
2590         if (!IS_DF_GRID(element))
2591           AddLaserEdge(LX, LY);
2592       }
2593       else
2594       {
2595         LY += YS;
2596         if (!IS_DF_GRID(element))
2597           AddLaserEdge(LX + XS / 2, LY);
2598
2599         LY += YS;
2600         if (!IS_DF_GRID(element))
2601           AddLaserEdge(LX, LY);
2602       }
2603
2604       XS = 2 * Step[laser.current_angle].x;
2605
2606       return FALSE;
2607     }
2608   }
2609
2610   // reflection at the edge of reflecting DF style wall
2611   if (IS_DF_WALL_STEEL(element) && IS_22_5_ANGLE(laser.current_angle))
2612   {
2613     if (((laser.current_angle == 1 || laser.current_angle == 3) &&
2614          hit_mask == HIT_MASK_TOPRIGHT) ||
2615         ((laser.current_angle == 5 || laser.current_angle == 7) &&
2616          hit_mask == HIT_MASK_TOPLEFT) ||
2617         ((laser.current_angle == 9 || laser.current_angle == 11) &&
2618          hit_mask == HIT_MASK_BOTTOMLEFT) ||
2619         ((laser.current_angle == 13 || laser.current_angle == 15) &&
2620          hit_mask == HIT_MASK_BOTTOMRIGHT))
2621     {
2622       int mirror_angle =
2623         (hit_mask == HIT_MASK_TOPRIGHT || hit_mask == HIT_MASK_BOTTOMLEFT ?
2624          ANG_MIRROR_135 : ANG_MIRROR_45);
2625
2626       PlayLevelSound_MM(ELX, ELY, element, MM_ACTION_HITTING);
2627
2628       AddDamagedField(ELX, ELY);
2629       AddLaserEdge(LX, LY);
2630
2631       laser.current_angle = get_mirrored_angle(laser.current_angle,
2632                                                mirror_angle);
2633       XS = 8 / -XS;
2634       YS = 8 / -YS;
2635
2636       LX += XS;
2637       LY += YS;
2638
2639       AddLaserEdge(LX, LY);
2640
2641       return FALSE;
2642     }
2643   }
2644
2645   // reflection inside an edge of reflecting DF style wall
2646   if (IS_DF_WALL_STEEL(element) && IS_22_5_ANGLE(laser.current_angle))
2647   {
2648     if (((laser.current_angle == 1 || laser.current_angle == 3) &&
2649          hit_mask == (HIT_MASK_ALL ^ HIT_MASK_BOTTOMLEFT)) ||
2650         ((laser.current_angle == 5 || laser.current_angle == 7) &&
2651          hit_mask == (HIT_MASK_ALL ^ HIT_MASK_BOTTOMRIGHT)) ||
2652         ((laser.current_angle == 9 || laser.current_angle == 11) &&
2653          hit_mask == (HIT_MASK_ALL ^ HIT_MASK_TOPRIGHT)) ||
2654         ((laser.current_angle == 13 || laser.current_angle == 15) &&
2655          hit_mask == (HIT_MASK_ALL ^ HIT_MASK_TOPLEFT)))
2656     {
2657       int mirror_angle =
2658         (hit_mask == (HIT_MASK_ALL ^ HIT_MASK_BOTTOMLEFT) ||
2659          hit_mask == (HIT_MASK_ALL ^ HIT_MASK_TOPRIGHT) ?
2660          ANG_MIRROR_135 : ANG_MIRROR_45);
2661
2662       PlayLevelSound_MM(ELX, ELY, element, MM_ACTION_HITTING);
2663
2664       /*
2665       AddDamagedField(ELX, ELY);
2666       */
2667
2668       AddLaserEdge(LX - XS, LY - YS);
2669       AddLaserEdge(LX - XS + (ABS(XS) == 4 ? XS/2 : 0),
2670                    LY - YS + (ABS(YS) == 4 ? YS/2 : 0));
2671
2672       laser.current_angle = get_mirrored_angle(laser.current_angle,
2673                                                mirror_angle);
2674       XS = 8 / -XS;
2675       YS = 8 / -YS;
2676
2677       LX += XS;
2678       LY += YS;
2679
2680       AddLaserEdge(LX, LY);
2681
2682       return FALSE;
2683     }
2684   }
2685
2686   // check if laser hits DF style wall with an angle of 90°
2687   if (IS_DF_WALL(element) && IS_90_ANGLE(laser.current_angle))
2688   {
2689     if ((IS_HORIZ_ANGLE(laser.current_angle) &&
2690          (!(hit_mask & HIT_MASK_TOP) || !(hit_mask & HIT_MASK_BOTTOM))) ||
2691         (IS_VERT_ANGLE(laser.current_angle) &&
2692          (!(hit_mask & HIT_MASK_LEFT) || !(hit_mask & HIT_MASK_RIGHT))))
2693     {
2694       // laser at last step touched nothing or the same side of the wall
2695       if (LX != last_LX || LY != last_LY || hit_mask == last_hit_mask)
2696       {
2697         AddDamagedField(ELX, ELY);
2698
2699         LX += 8 * XS;
2700         LY += 8 * YS;
2701
2702         last_LX = LX;
2703         last_LY = LY;
2704         last_hit_mask = hit_mask;
2705
2706         return FALSE;
2707       }
2708     }
2709   }
2710
2711   if (!HitOnlyAnEdge(hit_mask))
2712   {
2713     laser.overloaded = TRUE;
2714
2715     return TRUE;
2716   }
2717
2718   return FALSE;
2719 }
2720
2721 static boolean HitAbsorbingWalls(int element, int hit_mask)
2722 {
2723   if (HitOnlyAnEdge(hit_mask))
2724     return FALSE;
2725
2726   if (ABS(XS) == 4 &&
2727       (hit_mask == HIT_MASK_LEFT || hit_mask == HIT_MASK_RIGHT))
2728   {
2729     AddLaserEdge(LX - XS, LY - YS);
2730
2731     LX = LX + XS / 2;
2732     LY = LY + YS;
2733   }
2734
2735   if (ABS(YS) == 4 &&
2736       (hit_mask == HIT_MASK_TOP || hit_mask == HIT_MASK_BOTTOM))
2737   {
2738     AddLaserEdge(LX - XS, LY - YS);
2739
2740     LX = LX + XS;
2741     LY = LY + YS / 2;
2742   }
2743
2744   if (IS_WALL_WOOD(element) ||
2745       IS_DF_WALL_WOOD(element) ||
2746       IS_GRID_WOOD(element) ||
2747       IS_GRID_WOOD_FIXED(element) ||
2748       IS_GRID_WOOD_AUTO(element) ||
2749       element == EL_FUSE_ON ||
2750       element == EL_BLOCK_WOOD ||
2751       element == EL_GATE_WOOD)
2752   {
2753     PlayLevelSound_MM(ELX, ELY, element, MM_ACTION_HITTING);
2754
2755     return TRUE;
2756   }
2757
2758   if (IS_WALL_ICE(element))
2759   {
2760     int lx = LX + XS;
2761     int ly = LY + YS;
2762     int mask;
2763
2764     // check if laser hit adjacent edges of two diagonal tiles
2765     if (ELX != lx / TILEX)
2766       lx = LX - XS;
2767     if (ELY != ly / TILEY)
2768       ly = LY - YS;
2769
2770     mask =     lx / MINI_TILEX - ELX * 2 + 1;    // Quadrant (horizontal)
2771     mask <<= ((ly / MINI_TILEY - ELY * 2) > 0 ? 2 : 0);  // || (vertical)
2772
2773     // check if laser hits wall with an angle of 90°
2774     if (IS_90_ANGLE(laser.current_angle))
2775       mask += mask * (2 + IS_HORIZ_ANGLE(laser.current_angle) * 2);
2776
2777     if (mask == 1 || mask == 2 || mask == 4 || mask == 8)
2778     {
2779       int i;
2780
2781       for (i = 0; i < 4; i++)
2782       {
2783         if (mask == (1 << i) && (XS > 0) == (i % 2) && (YS > 0) == (i / 2))
2784           mask = 15 - (8 >> i);
2785         else if (ABS(XS) == 4 &&
2786                  mask == (1 << i) &&
2787                  (XS > 0) == (i % 2) &&
2788                  (YS < 0) == (i / 2))
2789           mask = 3 + (i / 2) * 9;
2790         else if (ABS(YS) == 4 &&
2791                  mask == (1 << i) &&
2792                  (XS < 0) == (i % 2) &&
2793                  (YS > 0) == (i / 2))
2794           mask = 5 + (i % 2) * 5;
2795       }
2796     }
2797
2798     laser.wall_mask = mask;
2799   }
2800   else if (IS_WALL_AMOEBA(element))
2801   {
2802     int elx = (LX - 2 * XS) / TILEX;
2803     int ely = (LY - 2 * YS) / TILEY;
2804     int element2 = Tile[elx][ely];
2805     int mask;
2806
2807     if (element2 != EL_EMPTY && !IS_WALL_AMOEBA(element2))
2808     {
2809       laser.dest_element = EL_EMPTY;
2810
2811       return TRUE;
2812     }
2813
2814     ELX = elx;
2815     ELY = ely;
2816
2817     mask = (LX - 2 * XS) / 16 - ELX * 2 + 1;
2818     mask <<= ((LY - 2 * YS) / 16 - ELY * 2) * 2;
2819
2820     if (IS_90_ANGLE(laser.current_angle))
2821       mask += mask * (2 + IS_HORIZ_ANGLE(laser.current_angle) * 2);
2822
2823     laser.dest_element = element2 | EL_WALL_AMOEBA_BASE;
2824
2825     laser.wall_mask = mask;
2826   }
2827
2828   return TRUE;
2829 }
2830
2831 static void OpenExit(int x, int y)
2832 {
2833   int delay = 6;
2834
2835   if (!MovDelay[x][y])          // next animation frame
2836     MovDelay[x][y] = 4 * delay;
2837
2838   if (MovDelay[x][y])           // wait some time before next frame
2839   {
2840     int phase;
2841
2842     MovDelay[x][y]--;
2843     phase = MovDelay[x][y] / delay;
2844
2845     if (!(MovDelay[x][y] % delay) && IN_SCR_FIELD(x, y))
2846       DrawGraphicAnimation_MM(x, y, IMG_MM_EXIT_OPENING, 3 - phase);
2847
2848     if (!MovDelay[x][y])
2849     {
2850       Tile[x][y] = EL_EXIT_OPEN;
2851       DrawField_MM(x, y);
2852     }
2853   }
2854 }
2855
2856 static void OpenGrayBall(int x, int y)
2857 {
2858   int delay = 2;
2859
2860   if (!MovDelay[x][y])          // next animation frame
2861   {
2862     if (IS_WALL(Store[x][y]))
2863     {
2864       DrawWalls_MM(x, y, Store[x][y]);
2865
2866       // copy wall tile to spare bitmap for "melting" animation
2867       BlitBitmap(drawto_mm, bitmap_db_field, cSX + x * TILEX, cSY + y * TILEY,
2868                  TILEX, TILEY, x * TILEX, y * TILEY);
2869
2870       DrawElement_MM(x, y, EL_GRAY_BALL);
2871     }
2872
2873     MovDelay[x][y] = 50 * delay;
2874   }
2875
2876   if (MovDelay[x][y])           // wait some time before next frame
2877   {
2878     MovDelay[x][y]--;
2879
2880     if (!(MovDelay[x][y] % delay) && IN_SCR_FIELD(x, y))
2881     {
2882       Bitmap *bitmap;
2883       int gx, gy;
2884       int dx = RND(26), dy = RND(26);
2885
2886       if (IS_WALL(Store[x][y]))
2887       {
2888         // copy wall tile from spare bitmap for "melting" animation
2889         bitmap = bitmap_db_field;
2890         gx = x * TILEX;
2891         gy = y * TILEY;
2892       }
2893       else
2894       {
2895         int graphic = el2gfx(Store[x][y]);
2896
2897         getGraphicSource(graphic, 0, &bitmap, &gx, &gy);
2898       }
2899
2900       BlitBitmap(bitmap, drawto_mm, gx + dx, gy + dy, 6, 6,
2901                  cSX + x * TILEX + dx, cSY + y * TILEY + dy);
2902
2903       laser.redraw = TRUE;
2904
2905       MarkTileDirty(x, y);
2906     }
2907
2908     if (!MovDelay[x][y])
2909     {
2910       Tile[x][y] = Store[x][y];
2911       Store[x][y] = Store2[x][y] = 0;
2912       MovDir[x][y] = MovPos[x][y] = MovDelay[x][y] = 0;
2913
2914       InitField(x, y, FALSE);
2915       DrawField_MM(x, y);
2916
2917       ScanLaser_FromLastMirror();
2918     }
2919   }
2920 }
2921
2922 static void OpenEnvelope(int x, int y)
2923 {
2924   int num_frames = 8;           // seven frames plus final empty space
2925
2926   if (!MovDelay[x][y])          // next animation frame
2927     MovDelay[x][y] = num_frames;
2928
2929   if (MovDelay[x][y])           // wait some time before next frame
2930   {
2931     int nr = ENVELOPE_OPENING_NR(Tile[x][y]);
2932
2933     MovDelay[x][y]--;
2934
2935     if (MovDelay[x][y] > 0 && IN_SCR_FIELD(x, y))
2936     {
2937       int graphic = el_act2gfx(EL_ENVELOPE_1 + nr, MM_ACTION_COLLECTING);
2938       int frame = num_frames - MovDelay[x][y] - 1;
2939
2940       DrawGraphicAnimation_MM(x, y, graphic, frame);
2941
2942       laser.redraw = TRUE;
2943     }
2944
2945     if (MovDelay[x][y] == 0)
2946     {
2947       Tile[x][y] = EL_EMPTY;
2948
2949       DrawField_MM(x, y);
2950
2951       ScanLaser();
2952
2953       ShowEnvelope(nr);
2954     }
2955   }
2956 }
2957
2958 static void MeltIce(int x, int y)
2959 {
2960   int frames = 5;
2961   int delay = 5;
2962
2963   if (!MovDelay[x][y])          // next animation frame
2964     MovDelay[x][y] = frames * delay;
2965
2966   if (MovDelay[x][y])           // wait some time before next frame
2967   {
2968     int phase;
2969     int wall_mask = Store2[x][y];
2970     int real_element = Tile[x][y] - EL_WALL_CHANGING_BASE + EL_WALL_ICE_BASE;
2971
2972     MovDelay[x][y]--;
2973     phase = frames - MovDelay[x][y] / delay - 1;
2974
2975     if (!MovDelay[x][y])
2976     {
2977       Tile[x][y] = real_element & (wall_mask ^ 0xFF);
2978       Store[x][y] = Store2[x][y] = 0;
2979
2980       DrawWalls_MM(x, y, Tile[x][y]);
2981
2982       if (Tile[x][y] == EL_WALL_ICE_BASE)
2983         Tile[x][y] = EL_EMPTY;
2984
2985       ScanLaser_FromLastMirror();
2986     }
2987     else if (!(MovDelay[x][y] % delay) && IN_SCR_FIELD(x, y))
2988     {
2989       DrawWallsAnimation_MM(x, y, real_element, phase, wall_mask);
2990
2991       laser.redraw = TRUE;
2992     }
2993   }
2994 }
2995
2996 static void GrowAmoeba(int x, int y)
2997 {
2998   int frames = 5;
2999   int delay = 1;
3000
3001   if (!MovDelay[x][y])          // next animation frame
3002     MovDelay[x][y] = frames * delay;
3003
3004   if (MovDelay[x][y])           // wait some time before next frame
3005   {
3006     int phase;
3007     int wall_mask = Store2[x][y];
3008     int real_element = Tile[x][y] - EL_WALL_CHANGING_BASE + EL_WALL_AMOEBA_BASE;
3009
3010     MovDelay[x][y]--;
3011     phase = MovDelay[x][y] / delay;
3012
3013     if (!MovDelay[x][y])
3014     {
3015       Tile[x][y] = real_element;
3016       Store[x][y] = Store2[x][y] = 0;
3017
3018       DrawWalls_MM(x, y, Tile[x][y]);
3019       DrawLaser(0, DL_LASER_ENABLED);
3020     }
3021     else if (!(MovDelay[x][y] % delay) && IN_SCR_FIELD(x, y))
3022     {
3023       DrawWallsAnimation_MM(x, y, real_element, phase, wall_mask);
3024     }
3025   }
3026 }
3027
3028 static void DrawFieldAnimated_MM(int x, int y)
3029 {
3030   DrawField_MM(x, y);
3031
3032   laser.redraw = TRUE;
3033 }
3034
3035 static void DrawFieldAnimatedIfNeeded_MM(int x, int y)
3036 {
3037   int element = Tile[x][y];
3038   int graphic = el2gfx(element);
3039
3040   if (!getGraphicInfo_NewFrame(x, y, graphic))
3041     return;
3042
3043   DrawField_MM(x, y);
3044
3045   laser.redraw = TRUE;
3046 }
3047
3048 static void DrawFieldTwinkle(int x, int y)
3049 {
3050   if (MovDelay[x][y] != 0)      // wait some time before next frame
3051   {
3052     MovDelay[x][y]--;
3053
3054     DrawField_MM(x, y);
3055
3056     if (MovDelay[x][y] != 0)
3057     {
3058       int graphic = IMG_TWINKLE_WHITE;
3059       int frame = getGraphicAnimationFrame(graphic, 10 - MovDelay[x][y]);
3060
3061       DrawGraphicThruMask_MM(SCREENX(x), SCREENY(y), graphic, frame);
3062     }
3063
3064     laser.redraw = TRUE;
3065   }
3066 }
3067
3068 static void Explode_MM(int x, int y, int phase, int mode)
3069 {
3070   int num_phase = 9, delay = 2;
3071   int last_phase = num_phase * delay;
3072   int half_phase = (num_phase / 2) * delay;
3073   int center_element;
3074
3075   laser.redraw = TRUE;
3076
3077   if (phase == EX_PHASE_START)          // initialize 'Store[][]' field
3078   {
3079     center_element = Tile[x][y];
3080
3081     if (IS_MOVING(x, y) || IS_BLOCKED(x, y))
3082     {
3083       // put moving element to center field (and let it explode there)
3084       center_element = MovingOrBlocked2Element_MM(x, y);
3085       RemoveMovingField_MM(x, y);
3086
3087       Tile[x][y] = center_element;
3088     }
3089
3090     if (center_element != EL_GRAY_BALL_ACTIVE)
3091       Store[x][y] = EL_EMPTY;
3092     Store2[x][y] = center_element;
3093
3094     Tile[x][y] = EL_EXPLODING_OPAQUE;
3095
3096     GfxElement[x][y] = (center_element == EL_BOMB_ACTIVE ? EL_BOMB :
3097                         center_element == EL_GRAY_BALL_ACTIVE ? EL_GRAY_BALL :
3098                         IS_MCDUFFIN(center_element) ? EL_MCDUFFIN :
3099                         center_element);
3100
3101     MovDir[x][y] = MovPos[x][y] = MovDelay[x][y] = 0;
3102
3103     ExplodePhase[x][y] = 1;
3104
3105     return;
3106   }
3107
3108   if (phase == 1)
3109     GfxFrame[x][y] = 0;         // restart explosion animation
3110
3111   ExplodePhase[x][y] = (phase < last_phase ? phase + 1 : 0);
3112
3113   center_element = Store2[x][y];
3114
3115   if (phase == half_phase && Store[x][y] == EL_EMPTY)
3116   {
3117     Tile[x][y] = EL_EXPLODING_TRANSP;
3118
3119     if (x == ELX && y == ELY)
3120       ScanLaser();
3121   }
3122
3123   if (phase == last_phase)
3124   {
3125     if (center_element == EL_BOMB_ACTIVE)
3126     {
3127       DrawLaser(0, DL_LASER_DISABLED);
3128       InitLaser();
3129
3130       Bang_MM(laser.start_edge.x, laser.start_edge.y);
3131
3132       laser.overloaded = FALSE;
3133     }
3134     else if (IS_MCDUFFIN(center_element) || IS_LASER(center_element))
3135     {
3136       GameOver_MM(GAME_OVER_BOMB);
3137     }
3138
3139     Tile[x][y] = Store[x][y];
3140
3141     Store[x][y] = Store2[x][y] = 0;
3142     MovDir[x][y] = MovPos[x][y] = MovDelay[x][y] = 0;
3143
3144     InitField(x, y, FALSE);
3145     DrawField_MM(x, y);
3146
3147     if (center_element == EL_GRAY_BALL_ACTIVE)
3148       ScanLaser_FromLastMirror();
3149   }
3150   else if (!(phase % delay) && IN_SCR_FIELD(SCREENX(x), SCREENY(y)))
3151   {
3152     int graphic = el_act2gfx(GfxElement[x][y], MM_ACTION_EXPLODING);
3153     int frame = getGraphicAnimationFrameXY(graphic, x, y);
3154
3155     DrawGraphicAnimation_MM(x, y, graphic, frame);
3156
3157     MarkTileDirty(x, y);
3158   }
3159 }
3160
3161 static void Bang_MM(int x, int y)
3162 {
3163   int element = Tile[x][y];
3164
3165   if (IS_PACMAN(element))
3166     PlayLevelSound_MM(x, y, element, MM_ACTION_EXPLODING);
3167   else if (element == EL_BOMB_ACTIVE || IS_MCDUFFIN(element))
3168     PlayLevelSound_MM(x, y, element, MM_ACTION_EXPLODING);
3169   else if (element == EL_KEY)
3170     PlayLevelSound_MM(x, y, element, MM_ACTION_EXPLODING);
3171   else
3172     PlayLevelSound_MM(x, y, element, MM_ACTION_EXPLODING);
3173
3174   Explode_MM(x, y, EX_PHASE_START, EX_TYPE_NORMAL);
3175 }
3176
3177 static void TurnRound(int x, int y)
3178 {
3179   static struct
3180   {
3181     int x, y;
3182   } move_xy[] =
3183   {
3184     {  0,  0 },
3185     { -1,  0 },
3186     { +1,  0 },
3187     {  0,  0 },
3188     {  0, -1 },
3189     {  0,  0 }, { 0, 0 }, { 0, 0 },
3190     {  0, +1 }
3191   };
3192   static struct
3193   {
3194     int left, right, back;
3195   } turn[] =
3196   {
3197     { 0,        0,              0        },
3198     { MV_DOWN,  MV_UP,          MV_RIGHT },
3199     { MV_UP,    MV_DOWN,        MV_LEFT  },
3200     { 0,        0,              0        },
3201     { MV_LEFT,  MV_RIGHT,       MV_DOWN  },
3202     { 0,        0,              0        },
3203     { 0,        0,              0        },
3204     { 0,        0,              0        },
3205     { MV_RIGHT, MV_LEFT,        MV_UP    }
3206   };
3207
3208   int element = Tile[x][y];
3209   int old_move_dir = MovDir[x][y];
3210   int right_dir = turn[old_move_dir].right;
3211   int back_dir = turn[old_move_dir].back;
3212   int right_dx = move_xy[right_dir].x, right_dy = move_xy[right_dir].y;
3213   int right_x = x + right_dx, right_y = y + right_dy;
3214
3215   if (element == EL_PACMAN)
3216   {
3217     boolean can_turn_right = FALSE;
3218
3219     if (IN_LEV_FIELD(right_x, right_y) &&
3220         IS_EATABLE4PACMAN(Tile[right_x][right_y]))
3221       can_turn_right = TRUE;
3222
3223     if (can_turn_right)
3224       MovDir[x][y] = right_dir;
3225     else
3226       MovDir[x][y] = back_dir;
3227
3228     MovDelay[x][y] = 0;
3229   }
3230 }
3231
3232 static void StartMoving_MM(int x, int y)
3233 {
3234   int element = Tile[x][y];
3235
3236   if (Stop[x][y])
3237     return;
3238
3239   if (CAN_MOVE(element))
3240   {
3241     int newx, newy;
3242
3243     if (MovDelay[x][y])         // wait some time before next movement
3244     {
3245       MovDelay[x][y]--;
3246
3247       if (MovDelay[x][y])
3248         return;
3249     }
3250
3251     // now make next step
3252
3253     Moving2Blocked(x, y, &newx, &newy); // get next screen position
3254
3255     if (element == EL_PACMAN &&
3256         IN_LEV_FIELD(newx, newy) && IS_EATABLE4PACMAN(Tile[newx][newy]) &&
3257         !ObjHit(newx, newy, HIT_POS_CENTER))
3258     {
3259       Store[newx][newy] = Tile[newx][newy];
3260       Tile[newx][newy] = EL_EMPTY;
3261
3262       DrawField_MM(newx, newy);
3263     }
3264     else if (!IN_LEV_FIELD(newx, newy) || !IS_FREE(newx, newy) ||
3265              ObjHit(newx, newy, HIT_POS_CENTER))
3266     {
3267       // object was running against a wall
3268
3269       TurnRound(x, y);
3270
3271       return;
3272     }
3273
3274     InitMovingField_MM(x, y, MovDir[x][y]);
3275   }
3276
3277   if (MovDir[x][y])
3278     ContinueMoving_MM(x, y);
3279 }
3280
3281 static void ContinueMoving_MM(int x, int y)
3282 {
3283   int element = Tile[x][y];
3284   int direction = MovDir[x][y];
3285   int dx = (direction == MV_LEFT ? -1 : direction == MV_RIGHT ? +1 : 0);
3286   int dy = (direction == MV_UP   ? -1 : direction == MV_DOWN  ? +1 : 0);
3287   int horiz_move = (dx!=0);
3288   int newx = x + dx, newy = y + dy;
3289   int step = (horiz_move ? dx : dy) * TILEX / 8;
3290
3291   MovPos[x][y] += step;
3292
3293   if (ABS(MovPos[x][y]) >= TILEX)       // object reached its destination
3294   {
3295     Tile[x][y] = EL_EMPTY;
3296     Tile[newx][newy] = element;
3297
3298     MovPos[x][y] = MovDir[x][y] = MovDelay[x][y] = 0;
3299     MovDelay[newx][newy] = 0;
3300
3301     if (!CAN_MOVE(element))
3302       MovDir[newx][newy] = 0;
3303
3304     DrawField_MM(x, y);
3305     DrawField_MM(newx, newy);
3306
3307     Stop[newx][newy] = TRUE;
3308
3309     if (element == EL_PACMAN)
3310     {
3311       if (Store[newx][newy] == EL_BOMB)
3312         Bang_MM(newx, newy);
3313
3314       if (IS_WALL_AMOEBA(Store[newx][newy]) &&
3315           (LX + 2 * XS) / TILEX == newx &&
3316           (LY + 2 * YS) / TILEY == newy)
3317       {
3318         laser.num_edges--;
3319         ScanLaser();
3320       }
3321     }
3322   }
3323   else                          // still moving on
3324   {
3325     DrawField_MM(x, y);
3326   }
3327
3328   laser.redraw = TRUE;
3329 }
3330
3331 boolean ClickElement(int x, int y, int button)
3332 {
3333   static DelayCounter click_delay = { CLICK_DELAY };
3334   static boolean new_button = TRUE;
3335   boolean element_clicked = FALSE;
3336   int element;
3337
3338   if (button == -1)
3339   {
3340     // initialize static variables
3341     click_delay.count = 0;
3342     click_delay.value = CLICK_DELAY;
3343     new_button = TRUE;
3344
3345     return FALSE;
3346   }
3347
3348   // do not rotate objects hit by the laser after the game was solved
3349   if (game_mm.level_solved && Hit[x][y])
3350     return FALSE;
3351
3352   if (button == MB_RELEASED)
3353   {
3354     new_button = TRUE;
3355     click_delay.value = CLICK_DELAY;
3356
3357     // release eventually hold auto-rotating mirror
3358     RotateMirror(x, y, MB_RELEASED);
3359
3360     return FALSE;
3361   }
3362
3363   if (!FrameReached(&click_delay) && !new_button)
3364     return FALSE;
3365
3366   if (button == MB_MIDDLEBUTTON)        // middle button has no function
3367     return FALSE;
3368
3369   if (!IN_LEV_FIELD(x, y))
3370     return FALSE;
3371
3372   if (Tile[x][y] == EL_EMPTY)
3373     return FALSE;
3374
3375   element = Tile[x][y];
3376
3377   if (IS_MIRROR(element) ||
3378       IS_BEAMER(element) ||
3379       IS_POLAR(element) ||
3380       IS_POLAR_CROSS(element) ||
3381       IS_DF_MIRROR(element) ||
3382       IS_DF_MIRROR_AUTO(element))
3383   {
3384     RotateMirror(x, y, button);
3385
3386     element_clicked = TRUE;
3387   }
3388   else if (IS_MCDUFFIN(element))
3389   {
3390     boolean has_laser = (x == laser.start_edge.x && y == laser.start_edge.y);
3391
3392     if (has_laser && !laser.fuse_off)
3393       DrawLaser(0, DL_LASER_DISABLED);
3394
3395     element = get_rotated_element(element, BUTTON_ROTATION(button));
3396
3397     Tile[x][y] = element;
3398     DrawField_MM(x, y);
3399
3400     if (has_laser)
3401     {
3402       laser.start_angle = get_element_angle(element);
3403
3404       InitLaser();
3405
3406       if (!laser.fuse_off)
3407         ScanLaser();
3408     }
3409
3410     element_clicked = TRUE;
3411   }
3412   else if (element == EL_FUSE_ON && laser.fuse_off)
3413   {
3414     if (x != laser.fuse_x || y != laser.fuse_y)
3415       return FALSE;
3416
3417     laser.fuse_off = FALSE;
3418     laser.fuse_x = laser.fuse_y = -1;
3419
3420     DrawGraphic_MM(x, y, IMG_MM_FUSE_ACTIVE);
3421     ScanLaser();
3422
3423     element_clicked = TRUE;
3424   }
3425   else if (element == EL_FUSE_ON && !laser.fuse_off && new_button)
3426   {
3427     laser.fuse_off = TRUE;
3428     laser.fuse_x = x;
3429     laser.fuse_y = y;
3430     laser.overloaded = FALSE;
3431
3432     DrawLaser(0, DL_LASER_DISABLED);
3433     DrawGraphic_MM(x, y, IMG_MM_FUSE);
3434
3435     element_clicked = TRUE;
3436   }
3437   else if (element == EL_LIGHTBALL)
3438   {
3439     Bang_MM(x, y);
3440     RaiseScoreElement_MM(element);
3441     DrawLaser(0, DL_LASER_ENABLED);
3442
3443     element_clicked = TRUE;
3444   }
3445   else if (IS_ENVELOPE(element))
3446   {
3447     Tile[x][y] = EL_ENVELOPE_1_OPENING + ENVELOPE_NR(element);
3448
3449     element_clicked = TRUE;
3450   }
3451
3452   click_delay.value = (new_button ? CLICK_DELAY_FIRST : CLICK_DELAY);
3453   new_button = FALSE;
3454
3455   return element_clicked;
3456 }
3457
3458 static void RotateMirror(int x, int y, int button)
3459 {
3460   if (button == MB_RELEASED)
3461   {
3462     // release eventually hold auto-rotating mirror
3463     hold_x = -1;
3464     hold_y = -1;
3465
3466     return;
3467   }
3468
3469   if (IS_MIRROR(Tile[x][y]) ||
3470       IS_POLAR_CROSS(Tile[x][y]) ||
3471       IS_POLAR(Tile[x][y]) ||
3472       IS_BEAMER(Tile[x][y]) ||
3473       IS_DF_MIRROR(Tile[x][y]) ||
3474       IS_GRID_STEEL_AUTO(Tile[x][y]) ||
3475       IS_GRID_WOOD_AUTO(Tile[x][y]))
3476   {
3477     Tile[x][y] = get_rotated_element(Tile[x][y], BUTTON_ROTATION(button));
3478   }
3479   else if (IS_DF_MIRROR_AUTO(Tile[x][y]))
3480   {
3481     if (button == MB_LEFTBUTTON)
3482     {
3483       // left mouse button only for manual adjustment, no auto-rotating;
3484       // freeze mirror for until mouse button released
3485       hold_x = x;
3486       hold_y = y;
3487     }
3488     else if (button == MB_RIGHTBUTTON && (hold_x != x || hold_y != y))
3489     {
3490       Tile[x][y] = get_rotated_element(Tile[x][y], ROTATE_RIGHT);
3491     }
3492   }
3493
3494   if (IS_GRID_STEEL_AUTO(Tile[x][y]) || IS_GRID_WOOD_AUTO(Tile[x][y]))
3495   {
3496     int edge = Hit[x][y];
3497
3498     DrawField_MM(x, y);
3499
3500     if (edge > 0)
3501     {
3502       DrawLaser(edge - 1, DL_LASER_DISABLED);
3503       ScanLaser();
3504     }
3505   }
3506   else if (ObjHit(x, y, HIT_POS_CENTER))
3507   {
3508     int edge = Hit[x][y];
3509
3510     if (edge == 0)
3511     {
3512       Warn("RotateMirror: inconsistent field Hit[][]!\n");
3513
3514       edge = 1;
3515     }
3516
3517     DrawLaser(edge - 1, DL_LASER_DISABLED);
3518     ScanLaser();
3519   }
3520   else
3521   {
3522     int check = 1;
3523
3524     if (ObjHit(x, y, HIT_POS_EDGE | HIT_POS_BETWEEN))
3525       check = 2;
3526
3527     DrawField_MM(x, y);
3528
3529     if ((IS_BEAMER(Tile[x][y]) ||
3530          IS_POLAR(Tile[x][y]) ||
3531          IS_POLAR_CROSS(Tile[x][y])) && x == ELX && y == ELY)
3532     {
3533       if (IS_BEAMER(Tile[x][y]))
3534       {
3535 #if 0
3536         Debug("game:mm:RotateMirror", "TEST (%d, %d) [%d] [%d]",
3537               LX, LY, laser.beamer_edge, laser.beamer[1].num);
3538 #endif
3539
3540         if (check == 1)
3541           laser.num_edges--;
3542       }
3543
3544       ScanLaser();
3545
3546       check = 0;
3547     }
3548
3549     if (check == 2)
3550       DrawLaser(0, DL_LASER_ENABLED);
3551   }
3552 }
3553
3554 static void AutoRotateMirrors(void)
3555 {
3556   int x, y;
3557
3558   if (!FrameReached(&rotate_delay))
3559     return;
3560
3561   for (x = 0; x < lev_fieldx; x++)
3562   {
3563     for (y = 0; y < lev_fieldy; y++)
3564     {
3565       int element = Tile[x][y];
3566
3567       // do not rotate objects hit by the laser after the game was solved
3568       if (game_mm.level_solved && Hit[x][y])
3569         continue;
3570
3571       if (IS_DF_MIRROR_AUTO(element) ||
3572           IS_GRID_WOOD_AUTO(element) ||
3573           IS_GRID_STEEL_AUTO(element) ||
3574           element == EL_REFRACTOR)
3575       {
3576         RotateMirror(x, y, MB_RIGHTBUTTON);
3577
3578         laser.redraw = TRUE;
3579       }
3580     }
3581   }
3582 }
3583
3584 static boolean ObjHit(int obx, int oby, int bits)
3585 {
3586   int i;
3587
3588   obx *= TILEX;
3589   oby *= TILEY;
3590
3591   if (bits & HIT_POS_CENTER)
3592   {
3593     if (CheckLaserPixel(cSX + obx + 15,
3594                         cSY + oby + 15))
3595       return TRUE;
3596   }
3597
3598   if (bits & HIT_POS_EDGE)
3599   {
3600     for (i = 0; i < 4; i++)
3601       if (CheckLaserPixel(cSX + obx + 31 * (i % 2),
3602                           cSY + oby + 31 * (i / 2)))
3603         return TRUE;
3604   }
3605
3606   if (bits & HIT_POS_BETWEEN)
3607   {
3608     for (i = 0; i < 4; i++)
3609       if (CheckLaserPixel(cSX + 4 + obx + 22 * (i % 2),
3610                           cSY + 4 + oby + 22 * (i / 2)))
3611         return TRUE;
3612   }
3613
3614   return FALSE;
3615 }
3616
3617 static void DeletePacMan(int px, int py)
3618 {
3619   int i, j;
3620
3621   Bang_MM(px, py);
3622
3623   if (game_mm.num_pacman <= 1)
3624   {
3625     game_mm.num_pacman = 0;
3626     return;
3627   }
3628
3629   for (i = 0; i < game_mm.num_pacman; i++)
3630     if (game_mm.pacman[i].x == px && game_mm.pacman[i].y == py)
3631       break;
3632
3633   game_mm.num_pacman--;
3634
3635   for (j = i; j < game_mm.num_pacman; j++)
3636   {
3637     game_mm.pacman[j].x   = game_mm.pacman[j + 1].x;
3638     game_mm.pacman[j].y   = game_mm.pacman[j + 1].y;
3639     game_mm.pacman[j].dir = game_mm.pacman[j + 1].dir;
3640   }
3641 }
3642
3643 static void GameActions_MM_Ext(void)
3644 {
3645   int element;
3646   int x, y, i;
3647
3648   int r, d;
3649
3650   for (y = 0; y < lev_fieldy; y++) for (x = 0; x < lev_fieldx; x++)
3651     Stop[x][y] = FALSE;
3652
3653   for (y = 0; y < lev_fieldy; y++) for (x = 0; x < lev_fieldx; x++)
3654   {
3655     element = Tile[x][y];
3656
3657     if (!IS_MOVING(x, y) && CAN_MOVE(element))
3658       StartMoving_MM(x, y);
3659     else if (IS_MOVING(x, y))
3660       ContinueMoving_MM(x, y);
3661     else if (IS_EXPLODING(element))
3662       Explode_MM(x, y, ExplodePhase[x][y], EX_TYPE_NORMAL);
3663     else if (element == EL_EXIT_OPENING)
3664       OpenExit(x, y);
3665     else if (element == EL_GRAY_BALL_OPENING)
3666       OpenGrayBall(x, y);
3667     else if (IS_ENVELOPE_OPENING(element))
3668       OpenEnvelope(x, y);
3669     else if (IS_WALL_CHANGING(element) && Store[x][y] == EL_WALL_ICE_BASE)
3670       MeltIce(x, y);
3671     else if (IS_WALL_CHANGING(element) && Store[x][y] == EL_WALL_AMOEBA_BASE)
3672       GrowAmoeba(x, y);
3673     else if (IS_MIRROR(element) ||
3674              IS_MIRROR_FIXED(element) ||
3675              element == EL_PRISM)
3676       DrawFieldTwinkle(x, y);
3677     else if (element == EL_GRAY_BALL_ACTIVE ||
3678              element == EL_BOMB_ACTIVE ||
3679              element == EL_MINE_ACTIVE)
3680       DrawFieldAnimated_MM(x, y);
3681     else if (!IS_BLOCKED(x, y))
3682       DrawFieldAnimatedIfNeeded_MM(x, y);
3683   }
3684
3685   AutoRotateMirrors();
3686
3687 #if 1
3688   // !!! CHANGE THIS: REDRAW ONLY WHEN NEEDED !!!
3689
3690   // redraw after Explode_MM() ...
3691   if (laser.redraw)
3692     DrawLaser(0, DL_LASER_ENABLED);
3693   laser.redraw = FALSE;
3694 #endif
3695
3696   CT = FrameCounter;
3697
3698   if (game_mm.num_pacman && FrameReached(&pacman_delay))
3699   {
3700     MovePacMen();
3701
3702     if (laser.num_damages > MAX_LASER_LEN && !laser.fuse_off)
3703     {
3704       DrawLaser(0, DL_LASER_DISABLED);
3705       ScanLaser();
3706     }
3707   }
3708
3709   // skip all following game actions if game is over
3710   if (game_mm.game_over)
3711     return;
3712
3713   if (game_mm.energy_left == 0 && !game.no_level_time_limit && game.time_limit)
3714   {
3715     FadeOutLaser();
3716
3717     GameOver_MM(GAME_OVER_NO_ENERGY);
3718
3719     return;
3720   }
3721
3722   if (FrameReached(&energy_delay))
3723   {
3724     if (game_mm.energy_left > 0)
3725       game_mm.energy_left--;
3726
3727     // when out of energy, wait another frame to play "out of time" sound
3728   }
3729
3730   element = laser.dest_element;
3731
3732 #if 0
3733   if (element != Tile[ELX][ELY])
3734   {
3735     Debug("game:mm:GameActions_MM_Ext", "element == %d, Tile[ELX][ELY] == %d",
3736           element, Tile[ELX][ELY]);
3737   }
3738 #endif
3739
3740   if (!laser.overloaded && laser.overload_value == 0 &&
3741       element != EL_BOMB &&
3742       element != EL_BOMB_ACTIVE &&
3743       element != EL_MINE &&
3744       element != EL_MINE_ACTIVE &&
3745       element != EL_GRAY_BALL &&
3746       element != EL_GRAY_BALL_ACTIVE &&
3747       element != EL_BLOCK_STONE &&
3748       element != EL_BLOCK_WOOD &&
3749       element != EL_FUSE_ON &&
3750       element != EL_FUEL_FULL &&
3751       !IS_WALL_ICE(element) &&
3752       !IS_WALL_AMOEBA(element))
3753     return;
3754
3755   overload_delay.value = HEALTH_DELAY(laser.overloaded);
3756
3757   if (((laser.overloaded && laser.overload_value < MAX_LASER_OVERLOAD) ||
3758        (!laser.overloaded && laser.overload_value > 0)) &&
3759       FrameReached(&overload_delay))
3760   {
3761     if (laser.overloaded)
3762       laser.overload_value++;
3763     else
3764       laser.overload_value--;
3765
3766     if (game_mm.cheat_no_overload)
3767     {
3768       laser.overloaded = FALSE;
3769       laser.overload_value = 0;
3770     }
3771
3772     game_mm.laser_overload_value = laser.overload_value;
3773
3774     if (laser.overload_value < MAX_LASER_OVERLOAD - 8)
3775     {
3776       SetLaserColor(0xFF);
3777
3778       DrawLaser(0, DL_LASER_ENABLED);
3779     }
3780
3781     if (!laser.overloaded)
3782       StopSound_MM(SND_MM_GAME_HEALTH_CHARGING);
3783     else if (setup.sound_loops)
3784       PlaySoundLoop_MM(SND_MM_GAME_HEALTH_CHARGING);
3785     else
3786       PlaySound_MM(SND_MM_GAME_HEALTH_CHARGING);
3787
3788     if (laser.overload_value == MAX_LASER_OVERLOAD)
3789     {
3790       UpdateAndDisplayGameControlValues();
3791
3792       FadeOutLaser();
3793
3794       GameOver_MM(GAME_OVER_OVERLOADED);
3795
3796       return;
3797     }
3798   }
3799
3800   if (laser.fuse_off)
3801     return;
3802
3803   CT -= Ct;
3804
3805   if (element == EL_BOMB && CT > native_mm_level.time_bomb)
3806   {
3807     if (game_mm.cheat_no_explosion)
3808       return;
3809
3810     Bang_MM(ELX, ELY);
3811
3812     laser.dest_element = EL_EXPLODING_OPAQUE;
3813
3814     return;
3815   }
3816
3817   if (element == EL_FUSE_ON && CT > native_mm_level.time_fuse)
3818   {
3819     laser.fuse_off = TRUE;
3820     laser.fuse_x = ELX;
3821     laser.fuse_y = ELY;
3822
3823     DrawLaser(0, DL_LASER_DISABLED);
3824     DrawGraphic_MM(ELX, ELY, IMG_MM_FUSE);
3825   }
3826
3827   if (element == EL_GRAY_BALL && CT > native_mm_level.time_ball)
3828   {
3829     if (!Store2[ELX][ELY])      // check if content element not yet determined
3830     {
3831       int last_anim_random_frame = gfx.anim_random_frame;
3832       int element_pos;
3833
3834       if (native_mm_level.ball_choice_mode == ANIM_RANDOM)
3835         gfx.anim_random_frame = RND(native_mm_level.num_ball_contents);
3836
3837       element_pos = getAnimationFrame(native_mm_level.num_ball_contents, 1,
3838                                       native_mm_level.ball_choice_mode, 0,
3839                                       game_mm.ball_choice_pos);
3840
3841       if (native_mm_level.ball_choice_mode == ANIM_RANDOM)
3842         gfx.anim_random_frame = last_anim_random_frame;
3843
3844       game_mm.ball_choice_pos++;
3845
3846       int new_element = native_mm_level.ball_content[element_pos];
3847       int new_element_base = map_wall_to_base_element(new_element);
3848
3849       if (IS_WALL(new_element_base))
3850       {
3851         // always use completely filled wall element
3852         new_element = new_element_base | 0x000f;
3853       }
3854       else if (native_mm_level.rotate_ball_content &&
3855                get_num_elements(new_element) > 1)
3856       {
3857         // randomly rotate newly created game element
3858         new_element = get_rotated_element(new_element, RND(16));
3859       }
3860
3861       Store[ELX][ELY] = new_element;
3862       Store2[ELX][ELY] = TRUE;
3863     }
3864
3865     if (native_mm_level.explode_ball)
3866       Bang_MM(ELX, ELY);
3867     else
3868       Tile[ELX][ELY] = EL_GRAY_BALL_OPENING;
3869
3870     laser.dest_element = laser.dest_element_last = Tile[ELX][ELY];
3871
3872     return;
3873   }
3874
3875   if (IS_WALL_ICE(element) && CT > 50)
3876   {
3877     PlayLevelSound_MM(ELX, ELY, element, MM_ACTION_SHRINKING);
3878
3879     Tile[ELX][ELY] = Tile[ELX][ELY] - EL_WALL_ICE_BASE + EL_WALL_CHANGING_BASE;
3880     Store[ELX][ELY] = EL_WALL_ICE_BASE;
3881     Store2[ELX][ELY] = laser.wall_mask;
3882
3883     laser.dest_element = Tile[ELX][ELY];
3884
3885     return;
3886   }
3887
3888   if (IS_WALL_AMOEBA(element) && CT > 60)
3889   {
3890     int k1, k2, k3;
3891     int element2 = Tile[ELX][ELY];
3892
3893     if (element2 != EL_EMPTY && !IS_WALL_AMOEBA(element))
3894       return;
3895
3896     for (i = laser.num_damages - 1; i >= 0; i--)
3897       if (laser.damage[i].is_mirror)
3898         break;
3899
3900     r = laser.num_edges;
3901     d = laser.num_damages;
3902     k1 = i;
3903
3904     if (k1 > 0)
3905     {
3906       int x, y;
3907
3908       DrawLaser(laser.damage[k1].edge - 1, DL_LASER_DISABLED);
3909
3910       laser.num_edges++;
3911       DrawLaser(0, DL_LASER_ENABLED);
3912       laser.num_edges--;
3913
3914       x = laser.damage[k1].x;
3915       y = laser.damage[k1].y;
3916
3917       DrawField_MM(x, y);
3918     }
3919
3920     for (i = 0; i < 4; i++)
3921     {
3922       if (laser.wall_mask & (1 << i))
3923       {
3924         if (CheckLaserPixel(cSX + ELX * TILEX + 14 + (i % 2) * 2,
3925                             cSY + ELY * TILEY + 31 * (i / 2)))
3926           break;
3927
3928         if (CheckLaserPixel(cSX + ELX * TILEX + 31 * (i % 2),
3929                             cSY + ELY * TILEY + 14 + (i / 2) * 2))
3930           break;
3931       }
3932     }
3933
3934     k2 = i;
3935
3936     for (i = 0; i < 4; i++)
3937     {
3938       if (laser.wall_mask & (1 << i))
3939       {
3940         if (CheckLaserPixel(cSX + ELX * TILEX + 31 * (i % 2),
3941                             cSY + ELY * TILEY + 31 * (i / 2)))
3942           break;
3943       }
3944     }
3945
3946     k3 = i;
3947
3948     if (laser.num_beamers > 0 ||
3949         k1 < 1 || k2 < 4 || k3 < 4 ||
3950         CheckLaserPixel(cSX + ELX * TILEX + 14,
3951                         cSY + ELY * TILEY + 14))
3952     {
3953       laser.num_edges = r;
3954       laser.num_damages = d;
3955
3956       DrawLaser(0, DL_LASER_DISABLED);
3957     }
3958
3959     Tile[ELX][ELY] = element | laser.wall_mask;
3960
3961     int x = ELX, y = ELY;
3962     int wall_mask = laser.wall_mask;
3963
3964     ScanLaser();
3965     DrawLaser(0, DL_LASER_ENABLED);
3966
3967     PlayLevelSound_MM(x, y, element, MM_ACTION_GROWING);
3968
3969     Tile[x][y] = Tile[x][y] - EL_WALL_AMOEBA_BASE + EL_WALL_CHANGING_BASE;
3970     Store[x][y] = EL_WALL_AMOEBA_BASE;
3971     Store2[x][y] = wall_mask;
3972
3973     return;
3974   }
3975
3976   if ((element == EL_BLOCK_WOOD || element == EL_BLOCK_STONE) &&
3977       laser.stops_inside_element && CT > native_mm_level.time_block)
3978   {
3979     int x, y;
3980     int k;
3981
3982     if (ABS(XS) > ABS(YS))
3983       k = 0;
3984     else
3985       k = 1;
3986     if (XS < YS)
3987       k += 2;
3988
3989     for (i = 0; i < 4; i++)
3990     {
3991       if (i)
3992         k++;
3993       if (k > 3)
3994         k = 0;
3995
3996       x = ELX + Step[k * 4].x;
3997       y = ELY + Step[k * 4].y;
3998
3999       if (!IN_LEV_FIELD(x, y) || Tile[x][y] != EL_EMPTY)
4000         continue;
4001
4002       if (ObjHit(x, y, HIT_POS_CENTER | HIT_POS_EDGE | HIT_POS_BETWEEN))
4003         continue;
4004
4005       break;
4006     }
4007
4008     if (i > 3)
4009     {
4010       laser.overloaded = (element == EL_BLOCK_STONE);
4011
4012       return;
4013     }
4014
4015     PlayLevelSound_MM(ELX, ELY, element, MM_ACTION_PUSHING);
4016
4017     Tile[ELX][ELY] = 0;
4018     Tile[x][y] = element;
4019
4020     DrawGraphic_MM(ELX, ELY, IMG_EMPTY);
4021     DrawField_MM(x, y);
4022
4023     if (element == EL_BLOCK_STONE && Box[ELX][ELY])
4024     {
4025       DrawLaser(Box[ELX][ELY] - 1, DL_LASER_DISABLED);
4026       DrawLaser(laser.num_edges - 1, DL_LASER_ENABLED);
4027     }
4028
4029     ScanLaser();
4030
4031     return;
4032   }
4033
4034   if (element == EL_FUEL_FULL && CT > 10)
4035   {
4036     int num_init_game_frames = INIT_GAME_ACTIONS_DELAY;
4037     int start = num_init_game_frames * game_mm.energy_left / native_mm_level.time;
4038
4039     for (i = start; i <= num_init_game_frames; i++)
4040     {
4041       if (i == num_init_game_frames)
4042         StopSound_MM(SND_MM_GAME_LEVELTIME_CHARGING);
4043       else if (setup.sound_loops)
4044         PlaySoundLoop_MM(SND_MM_GAME_LEVELTIME_CHARGING);
4045       else
4046         PlaySound_MM(SND_MM_GAME_LEVELTIME_CHARGING);
4047
4048       game_mm.energy_left = native_mm_level.time * i / num_init_game_frames;
4049
4050       UpdateAndDisplayGameControlValues();
4051
4052       BackToFront_MM();
4053     }
4054
4055     Tile[ELX][ELY] = laser.dest_element = EL_FUEL_EMPTY;
4056
4057     DrawField_MM(ELX, ELY);
4058
4059     DrawLaser(0, DL_LASER_ENABLED);
4060
4061     return;
4062   }
4063 }
4064
4065 void GameActions_MM(struct MouseActionInfo action)
4066 {
4067   boolean element_clicked = ClickElement(action.lx, action.ly, action.button);
4068   boolean button_released = (action.button == MB_RELEASED);
4069
4070   GameActions_MM_Ext();
4071
4072   CheckSingleStepMode_MM(element_clicked, button_released);
4073 }
4074
4075 static void MovePacMen(void)
4076 {
4077   int mx, my, ox, oy, nx, ny;
4078   int element;
4079   int l;
4080
4081   if (++pacman_nr >= game_mm.num_pacman)
4082     pacman_nr = 0;
4083
4084   game_mm.pacman[pacman_nr].dir--;
4085
4086   for (l = 1; l < 5; l++)
4087   {
4088     game_mm.pacman[pacman_nr].dir++;
4089
4090     if (game_mm.pacman[pacman_nr].dir > 4)
4091       game_mm.pacman[pacman_nr].dir = 1;
4092
4093     if (game_mm.pacman[pacman_nr].dir % 2)
4094     {
4095       mx = 0;
4096       my = game_mm.pacman[pacman_nr].dir - 2;
4097     }
4098     else
4099     {
4100       my = 0;
4101       mx = 3 - game_mm.pacman[pacman_nr].dir;
4102     }
4103
4104     ox = game_mm.pacman[pacman_nr].x;
4105     oy = game_mm.pacman[pacman_nr].y;
4106     nx = ox + mx;
4107     ny = oy + my;
4108     element = Tile[nx][ny];
4109
4110     if (nx < 0 || nx > 15 || ny < 0 || ny > 11)
4111       continue;
4112
4113     if (!IS_EATABLE4PACMAN(element))
4114       continue;
4115
4116     if (ObjHit(nx, ny, HIT_POS_CENTER))
4117       continue;
4118
4119     Tile[ox][oy] = EL_EMPTY;
4120     Tile[nx][ny] =
4121       EL_PACMAN_RIGHT - 1 +
4122       (game_mm.pacman[pacman_nr].dir - 1 +
4123        (game_mm.pacman[pacman_nr].dir % 2) * 2);
4124
4125     game_mm.pacman[pacman_nr].x = nx;
4126     game_mm.pacman[pacman_nr].y = ny;
4127
4128     DrawGraphic_MM(ox, oy, IMG_EMPTY);
4129
4130     if (element != EL_EMPTY)
4131     {
4132       int graphic = el2gfx(Tile[nx][ny]);
4133       Bitmap *bitmap;
4134       int src_x, src_y;
4135       int i;
4136
4137       getGraphicSource(graphic, 0, &bitmap, &src_x, &src_y);
4138
4139       CT = FrameCounter;
4140       ox = cSX + ox * TILEX;
4141       oy = cSY + oy * TILEY;
4142
4143       for (i = 1; i < 33; i += 2)
4144         BlitBitmap(bitmap, window,
4145                    src_x, src_y, TILEX, TILEY,
4146                    ox + i * mx, oy + i * my);
4147       Ct = Ct + FrameCounter - CT;
4148     }
4149
4150     DrawField_MM(nx, ny);
4151     BackToFront_MM();
4152
4153     if (!laser.fuse_off)
4154     {
4155       DrawLaser(0, DL_LASER_ENABLED);
4156
4157       if (ObjHit(nx, ny, HIT_POS_BETWEEN))
4158       {
4159         AddDamagedField(nx, ny);
4160
4161         laser.damage[laser.num_damages - 1].edge = 0;
4162       }
4163     }
4164
4165     if (element == EL_BOMB)
4166       DeletePacMan(nx, ny);
4167
4168     if (IS_WALL_AMOEBA(element) &&
4169         (LX + 2 * XS) / TILEX == nx &&
4170         (LY + 2 * YS) / TILEY == ny)
4171     {
4172       laser.num_edges--;
4173       ScanLaser();
4174     }
4175
4176     break;
4177   }
4178 }
4179
4180 static void InitMovingField_MM(int x, int y, int direction)
4181 {
4182   int newx = x + (direction == MV_LEFT ? -1 : direction == MV_RIGHT ? +1 : 0);
4183   int newy = y + (direction == MV_UP   ? -1 : direction == MV_DOWN  ? +1 : 0);
4184
4185   MovDir[x][y] = direction;
4186   MovDir[newx][newy] = direction;
4187
4188   if (Tile[newx][newy] == EL_EMPTY)
4189     Tile[newx][newy] = EL_BLOCKED;
4190 }
4191
4192 static int MovingOrBlocked2Element_MM(int x, int y)
4193 {
4194   int element = Tile[x][y];
4195
4196   if (element == EL_BLOCKED)
4197   {
4198     int oldx, oldy;
4199
4200     Blocked2Moving(x, y, &oldx, &oldy);
4201
4202     return Tile[oldx][oldy];
4203   }
4204
4205   return element;
4206 }
4207
4208 static void RemoveMovingField_MM(int x, int y)
4209 {
4210   int oldx = x, oldy = y, newx = x, newy = y;
4211
4212   if (Tile[x][y] != EL_BLOCKED && !IS_MOVING(x, y))
4213     return;
4214
4215   if (IS_MOVING(x, y))
4216   {
4217     Moving2Blocked(x, y, &newx, &newy);
4218     if (Tile[newx][newy] != EL_BLOCKED)
4219       return;
4220   }
4221   else if (Tile[x][y] == EL_BLOCKED)
4222   {
4223     Blocked2Moving(x, y, &oldx, &oldy);
4224     if (!IS_MOVING(oldx, oldy))
4225       return;
4226   }
4227
4228   Tile[oldx][oldy] = EL_EMPTY;
4229   Tile[newx][newy] = EL_EMPTY;
4230   MovPos[oldx][oldy] = MovDir[oldx][oldy] = MovDelay[oldx][oldy] = 0;
4231   MovPos[newx][newy] = MovDir[newx][newy] = MovDelay[newx][newy] = 0;
4232
4233   DrawLevelField_MM(oldx, oldy);
4234   DrawLevelField_MM(newx, newy);
4235 }
4236
4237 static void RaiseScore_MM(int value)
4238 {
4239   game_mm.score += value;
4240 }
4241
4242 void RaiseScoreElement_MM(int element)
4243 {
4244   switch (element)
4245   {
4246     case EL_PACMAN:
4247     case EL_PACMAN_RIGHT:
4248     case EL_PACMAN_UP:
4249     case EL_PACMAN_LEFT:
4250     case EL_PACMAN_DOWN:
4251       RaiseScore_MM(native_mm_level.score[SC_PACMAN]);
4252       break;
4253
4254     case EL_KEY:
4255       RaiseScore_MM(native_mm_level.score[SC_KEY]);
4256       break;
4257
4258     case EL_KETTLE:
4259     case EL_CELL:
4260       RaiseScore_MM(native_mm_level.score[SC_COLLECTIBLE]);
4261       break;
4262
4263     case EL_LIGHTBALL:
4264       RaiseScore_MM(native_mm_level.score[SC_LIGHTBALL]);
4265       break;
4266
4267     default:
4268       break;
4269   }
4270 }
4271
4272
4273 // ----------------------------------------------------------------------------
4274 // Mirror Magic game engine snapshot handling functions
4275 // ----------------------------------------------------------------------------
4276
4277 void SaveEngineSnapshotValues_MM(void)
4278 {
4279   int x, y;
4280
4281   engine_snapshot_mm.game_mm = game_mm;
4282   engine_snapshot_mm.laser = laser;
4283
4284   for (x = 0; x < MAX_PLAYFIELD_WIDTH; x++)
4285   {
4286     for (y = 0; y < MAX_PLAYFIELD_HEIGHT; y++)
4287     {
4288       engine_snapshot_mm.Ur[x][y]    = Ur[x][y];
4289       engine_snapshot_mm.Hit[x][y]   = Hit[x][y];
4290       engine_snapshot_mm.Box[x][y]   = Box[x][y];
4291       engine_snapshot_mm.Angle[x][y] = Angle[x][y];
4292     }
4293   }
4294
4295   engine_snapshot_mm.LX = LX;
4296   engine_snapshot_mm.LY = LY;
4297   engine_snapshot_mm.XS = XS;
4298   engine_snapshot_mm.YS = YS;
4299   engine_snapshot_mm.ELX = ELX;
4300   engine_snapshot_mm.ELY = ELY;
4301   engine_snapshot_mm.CT = CT;
4302   engine_snapshot_mm.Ct = Ct;
4303
4304   engine_snapshot_mm.last_LX = last_LX;
4305   engine_snapshot_mm.last_LY = last_LY;
4306   engine_snapshot_mm.last_hit_mask = last_hit_mask;
4307   engine_snapshot_mm.hold_x = hold_x;
4308   engine_snapshot_mm.hold_y = hold_y;
4309   engine_snapshot_mm.pacman_nr = pacman_nr;
4310
4311   engine_snapshot_mm.rotate_delay = rotate_delay;
4312   engine_snapshot_mm.pacman_delay = pacman_delay;
4313   engine_snapshot_mm.energy_delay = energy_delay;
4314   engine_snapshot_mm.overload_delay = overload_delay;
4315 }
4316
4317 void LoadEngineSnapshotValues_MM(void)
4318 {
4319   int x, y;
4320
4321   // stored engine snapshot buffers already restored at this point
4322
4323   game_mm = engine_snapshot_mm.game_mm;
4324   laser   = engine_snapshot_mm.laser;
4325
4326   for (x = 0; x < MAX_PLAYFIELD_WIDTH; x++)
4327   {
4328     for (y = 0; y < MAX_PLAYFIELD_HEIGHT; y++)
4329     {
4330       Ur[x][y]    = engine_snapshot_mm.Ur[x][y];
4331       Hit[x][y]   = engine_snapshot_mm.Hit[x][y];
4332       Box[x][y]   = engine_snapshot_mm.Box[x][y];
4333       Angle[x][y] = engine_snapshot_mm.Angle[x][y];
4334     }
4335   }
4336
4337   LX  = engine_snapshot_mm.LX;
4338   LY  = engine_snapshot_mm.LY;
4339   XS  = engine_snapshot_mm.XS;
4340   YS  = engine_snapshot_mm.YS;
4341   ELX = engine_snapshot_mm.ELX;
4342   ELY = engine_snapshot_mm.ELY;
4343   CT  = engine_snapshot_mm.CT;
4344   Ct  = engine_snapshot_mm.Ct;
4345
4346   last_LX       = engine_snapshot_mm.last_LX;
4347   last_LY       = engine_snapshot_mm.last_LY;
4348   last_hit_mask = engine_snapshot_mm.last_hit_mask;
4349   hold_x        = engine_snapshot_mm.hold_x;
4350   hold_y        = engine_snapshot_mm.hold_y;
4351   pacman_nr     = engine_snapshot_mm.pacman_nr;
4352
4353   rotate_delay   = engine_snapshot_mm.rotate_delay;
4354   pacman_delay   = engine_snapshot_mm.pacman_delay;
4355   energy_delay   = engine_snapshot_mm.energy_delay;
4356   overload_delay = engine_snapshot_mm.overload_delay;
4357
4358   RedrawPlayfield_MM();
4359 }
4360
4361 static int getAngleFromTouchDelta(int dx, int dy,  int base)
4362 {
4363   double pi = 3.141592653;
4364   double rad = atan2((double)-dy, (double)dx);
4365   double rad2 = (rad < 0 ? rad + 2 * pi : rad);
4366   double deg = rad2 * 180.0 / pi;
4367
4368   return (int)(deg * base / 360.0 + 0.5) % base;
4369 }
4370
4371 int getButtonFromTouchPosition(int x, int y, int dst_mx, int dst_my)
4372 {
4373   // calculate start (source) position to be at the middle of the tile
4374   int src_mx = cSX + x * TILESIZE_VAR + TILESIZE_VAR / 2;
4375   int src_my = cSY + y * TILESIZE_VAR + TILESIZE_VAR / 2;
4376   int dx = dst_mx - src_mx;
4377   int dy = dst_my - src_my;
4378   int element;
4379   int base = 16;
4380   int phases = 16;
4381   int angle_old = -1;
4382   int angle_new = -1;
4383   int button = 0;
4384   int i;
4385
4386   if (!IN_LEV_FIELD(x, y))
4387     return 0;
4388
4389   element = Tile[x][y];
4390
4391   if (!IS_MCDUFFIN(element) &&
4392       !IS_MIRROR(element) &&
4393       !IS_BEAMER(element) &&
4394       !IS_POLAR(element) &&
4395       !IS_POLAR_CROSS(element) &&
4396       !IS_DF_MIRROR(element))
4397     return 0;
4398
4399   angle_old = get_element_angle(element);
4400
4401   if (IS_MCDUFFIN(element))
4402   {
4403     angle_new = (dx > 0 && ABS(dy) < ABS(dx) ? ANG_RAY_RIGHT :
4404                  dy < 0 && ABS(dx) < ABS(dy) ? ANG_RAY_UP :
4405                  dx < 0 && ABS(dy) < ABS(dx) ? ANG_RAY_LEFT :
4406                  dy > 0 && ABS(dx) < ABS(dy) ? ANG_RAY_DOWN :
4407                  -1);
4408   }
4409   else if (IS_MIRROR(element) ||
4410            IS_DF_MIRROR(element))
4411   {
4412     for (i = 0; i < laser.num_damages; i++)
4413     {
4414       if (laser.damage[i].x == x &&
4415           laser.damage[i].y == y &&
4416           ObjHit(x, y, HIT_POS_CENTER))
4417       {
4418         angle_old = get_mirrored_angle(laser.damage[i].angle, angle_old);
4419         angle_new = getAngleFromTouchDelta(dx, dy, base) % phases;
4420
4421         break;
4422       }
4423     }
4424   }
4425
4426   if (angle_new == -1)
4427   {
4428     if (IS_MIRROR(element) ||
4429         IS_DF_MIRROR(element) ||
4430         IS_POLAR(element))
4431       base = 32;
4432
4433     if (IS_POLAR_CROSS(element))
4434       phases = 4;
4435
4436     angle_new = getAngleFromTouchDelta(dx, dy, base) % phases;
4437   }
4438
4439   button = (angle_new == angle_old ? 0 :
4440             (angle_new - angle_old + phases) % phases < (phases / 2) ?
4441             MB_LEFTBUTTON : MB_RIGHTBUTTON);
4442
4443   return button;
4444 }