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