1 // ============================================================================
2 // Mirror Magic -- McDuffin's Revenge
3 // ----------------------------------------------------------------------------
4 // (c) 1994-2017 by Artsoft Entertainment
7 // https://www.artsoft.org/
8 // ----------------------------------------------------------------------------
10 // ============================================================================
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
26 // values for Explode_MM()
27 #define EX_PHASE_START 0
28 #define EX_TYPE_NONE 0
29 #define EX_TYPE_NORMAL (1 << 0)
31 // special positions in the game control window (relative to control window)
40 #define XX_OVERLOAD 60
41 #define YY_OVERLOAD YY_ENERGY
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)
55 #define IS_LOOP_SOUND(s) ((s) == SND_FUEL)
56 #define IS_MUSIC_SOUND(s) ((s) == SND_TYGER || (s) == SND_VOYAGER)
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
63 #define NUM_GAME_BUTTONS 3
65 // values for DrawLaser()
66 #define DL_LASER_DISABLED 0
67 #define DL_LASER_ENABLED 1
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
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)
82 #define BEGIN_NO_HEADLESS \
84 boolean last_headless = program.headless; \
86 program.headless = FALSE; \
88 #define END_NO_HEADLESS \
89 program.headless = last_headless; \
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);
101 static void AddLaserEdge(int, int);
102 static void ScanLaser(void);
103 static void DrawLaser(int, int);
104 static boolean HitElement(int, int);
105 static boolean HitOnlyAnEdge(int);
106 static boolean HitPolarizer(int, int);
107 static boolean HitBlock(int, int);
108 static boolean HitLaserSource(int, int);
109 static boolean HitLaserDestination(int, int);
110 static boolean HitReflectingWalls(int, int);
111 static boolean HitAbsorbingWalls(int, int);
112 static void RotateMirror(int, int, int);
113 static boolean ObjHit(int, int, int);
114 static void DeletePacMan(int, int);
115 static void MovePacMen(void);
117 // bitmap for laser beam detection
118 static Bitmap *laser_bitmap = NULL;
120 // variables for laser control
121 static int last_LX = 0, last_LY = 0, last_hit_mask = 0;
122 static int hold_x = -1, hold_y = -1;
124 // variables for pacman control
125 static int pacman_nr = -1;
127 // various game engine delay counters
128 static DelayCounter rotate_delay = { AUTO_ROTATE_DELAY };
129 static DelayCounter pacman_delay = { PACMAN_MOVE_DELAY };
130 static DelayCounter energy_delay = { ENERGY_DELAY };
131 static DelayCounter overload_delay = { 0 };
133 // element mask positions for scanning pixels of MM elements
134 #define MM_MASK_MCDUFFIN_RIGHT 0
135 #define MM_MASK_MCDUFFIN_UP 1
136 #define MM_MASK_MCDUFFIN_LEFT 2
137 #define MM_MASK_MCDUFFIN_DOWN 3
138 #define MM_MASK_GRID_1 4
139 #define MM_MASK_GRID_2 5
140 #define MM_MASK_GRID_3 6
141 #define MM_MASK_GRID_4 7
142 #define MM_MASK_SLOPE_1 8
143 #define MM_MASK_SLOPE_2 9
144 #define MM_MASK_SLOPE_3 10
145 #define MM_MASK_SLOPE_4 11
146 #define MM_MASK_RECTANGLE 12
147 #define MM_MASK_CIRCLE 13
149 #define NUM_MM_MASKS 14
151 // element masks for scanning pixels of MM elements
152 static const char mm_masks[NUM_MM_MASKS][16][16 + 1] =
408 static int get_element_angle(int element)
410 int element_phase = get_element_phase(element);
412 if (IS_MIRROR_FIXED(element) ||
413 IS_MCDUFFIN(element) ||
415 IS_RECEIVER(element))
416 return 4 * element_phase;
417 else if (IS_DF_SLOPE(element))
418 return 4 + (element_phase % 2) * 8;
420 return element_phase;
423 static int get_opposite_angle(int angle)
425 int opposite_angle = angle + ANG_RAY_180;
427 // make sure "opposite_angle" is in valid interval [0, 15]
428 return (opposite_angle + 16) % 16;
431 static int get_mirrored_angle(int laser_angle, int mirror_angle)
433 int reflected_angle = 16 - laser_angle + mirror_angle;
435 // make sure "reflected_angle" is in valid interval [0, 15]
436 return (reflected_angle + 16) % 16;
439 static void DrawLaserLines(struct XY *points, int num_points, int mode)
441 Pixel pixel_drawto = (mode == DL_LASER_ENABLED ? pen_ray : pen_bg);
442 Pixel pixel_buffer = (mode == DL_LASER_ENABLED ? WHITE_PIXEL : BLACK_PIXEL);
444 DrawLines(drawto_mm, points, num_points, pixel_drawto);
448 DrawLines(laser_bitmap, points, num_points, pixel_buffer);
453 static boolean CheckLaserPixel(int x, int y)
459 pixel = ReadPixel(laser_bitmap, x, y);
463 return (pixel == WHITE_PIXEL);
466 static void CheckExitMM(void)
468 int exit_element = EL_EMPTY;
472 static int xy[4][2] =
480 for (y = 0; y < lev_fieldy; y++)
482 for (x = 0; x < lev_fieldx; x++)
484 if (Tile[x][y] == EL_EXIT_CLOSED)
486 // initiate opening animation of exit door
487 Tile[x][y] = EL_EXIT_OPENING;
489 exit_element = EL_EXIT_OPEN;
493 else if (IS_RECEIVER(Tile[x][y]))
495 // remove field that blocks receiver
496 int phase = Tile[x][y] - EL_RECEIVER_START;
497 int blocking_x, blocking_y;
499 blocking_x = x + xy[phase][0];
500 blocking_y = y + xy[phase][1];
502 if (IN_LEV_FIELD(blocking_x, blocking_y))
504 Tile[blocking_x][blocking_y] = EL_EMPTY;
506 DrawField_MM(blocking_x, blocking_y);
509 exit_element = EL_RECEIVER;
516 if (exit_element != EL_EMPTY)
517 PlayLevelSound_MM(exit_x, exit_y, exit_element, MM_ACTION_OPENING);
520 static void SetLaserColor(int brightness)
522 int color_min = 0x00;
523 int color_max = brightness; // (0x00 <= brightness <= 0xFF)
524 int color_up = color_max * laser.overload_value / MAX_LASER_OVERLOAD;
525 int color_down = color_max - color_up;
528 GetPixelFromRGB(window,
529 (game_mm.laser_red ? color_max : color_up),
530 (game_mm.laser_green ? color_down : color_min),
531 (game_mm.laser_blue ? color_down : color_min));
534 static void InitMovDir_MM(int x, int y)
536 int element = Tile[x][y];
537 static int direction[3][4] =
539 { MV_RIGHT, MV_UP, MV_LEFT, MV_DOWN },
540 { MV_LEFT, MV_DOWN, MV_RIGHT, MV_UP },
541 { MV_LEFT, MV_RIGHT, MV_UP, MV_DOWN }
546 case EL_PACMAN_RIGHT:
550 Tile[x][y] = EL_PACMAN;
551 MovDir[x][y] = direction[0][element - EL_PACMAN_RIGHT];
559 static void InitField(int x, int y, boolean init_game)
561 int element = Tile[x][y];
566 Tile[x][y] = EL_EMPTY;
571 if (init_game && native_mm_level.auto_count_kettles)
572 game_mm.kettles_still_needed++;
575 case EL_LIGHTBULB_OFF:
576 game_mm.lights_still_needed++;
580 if (IS_MIRROR(element) ||
581 IS_BEAMER_OLD(element) ||
582 IS_BEAMER(element) ||
584 IS_POLAR_CROSS(element) ||
585 IS_DF_MIRROR(element) ||
586 IS_DF_MIRROR_AUTO(element) ||
587 IS_GRID_STEEL_AUTO(element) ||
588 IS_GRID_WOOD_AUTO(element) ||
589 IS_FIBRE_OPTIC(element))
591 if (IS_BEAMER_OLD(element))
593 Tile[x][y] = EL_BEAMER_BLUE_START + (element - EL_BEAMER_START);
594 element = Tile[x][y];
597 if (!IS_FIBRE_OPTIC(element))
599 static int steps_grid_auto = 0;
601 if (game_mm.num_cycle == 0) // initialize cycle steps for grids
602 steps_grid_auto = RND(16) * (RND(2) ? -1 : +1);
604 if (IS_GRID_STEEL_AUTO(element) ||
605 IS_GRID_WOOD_AUTO(element))
606 game_mm.cycle[game_mm.num_cycle].steps = steps_grid_auto;
608 game_mm.cycle[game_mm.num_cycle].steps = RND(16) * (RND(2) ? -1 : +1);
610 game_mm.cycle[game_mm.num_cycle].x = x;
611 game_mm.cycle[game_mm.num_cycle].y = y;
615 if (IS_BEAMER(element) || IS_FIBRE_OPTIC(element))
617 int beamer_nr = BEAMER_NR(element);
618 int nr = laser.beamer[beamer_nr][0].num;
620 laser.beamer[beamer_nr][nr].x = x;
621 laser.beamer[beamer_nr][nr].y = y;
622 laser.beamer[beamer_nr][nr].num = 1;
625 else if (IS_PACMAN(element))
629 else if (IS_MCDUFFIN(element) || IS_LASER(element))
633 laser.start_edge.x = x;
634 laser.start_edge.y = y;
635 laser.start_angle = get_element_angle(element);
638 if (IS_MCDUFFIN(element))
640 game_mm.laser_red = native_mm_level.mm_laser_red;
641 game_mm.laser_green = native_mm_level.mm_laser_green;
642 game_mm.laser_blue = native_mm_level.mm_laser_blue;
646 game_mm.laser_red = native_mm_level.df_laser_red;
647 game_mm.laser_green = native_mm_level.df_laser_green;
648 game_mm.laser_blue = native_mm_level.df_laser_blue;
651 game_mm.has_mcduffin = (IS_MCDUFFIN(element));
658 static void InitCycleElements_RotateSingleStep(void)
662 if (game_mm.num_cycle == 0) // no elements to cycle
665 for (i = 0; i < game_mm.num_cycle; i++)
667 int x = game_mm.cycle[i].x;
668 int y = game_mm.cycle[i].y;
669 int step = SIGN(game_mm.cycle[i].steps);
670 int last_element = Tile[x][y];
671 int next_element = get_rotated_element(last_element, step);
673 if (!game_mm.cycle[i].steps)
676 Tile[x][y] = next_element;
678 game_mm.cycle[i].steps -= step;
682 static void InitLaser(void)
684 int start_element = Tile[laser.start_edge.x][laser.start_edge.y];
685 int step = (IS_LASER(start_element) ? 4 : 0);
687 LX = laser.start_edge.x * TILEX;
688 if (laser.start_angle == ANG_RAY_UP || laser.start_angle == ANG_RAY_DOWN)
691 LX += (laser.start_angle == ANG_RAY_RIGHT ? 28 + step : 0 - step);
693 LY = laser.start_edge.y * TILEY;
694 if (laser.start_angle == ANG_RAY_UP || laser.start_angle == ANG_RAY_DOWN)
695 LY += (laser.start_angle == ANG_RAY_DOWN ? 28 + step : 0 - step);
699 XS = 2 * Step[laser.start_angle].x;
700 YS = 2 * Step[laser.start_angle].y;
702 laser.current_angle = laser.start_angle;
704 laser.num_damages = 0;
706 laser.num_beamers = 0;
707 laser.beamer_edge[0] = 0;
709 laser.dest_element = EL_EMPTY;
712 AddLaserEdge(LX, LY); // set laser starting edge
717 void InitGameEngine_MM(void)
723 // initialize laser bitmap to current playfield (screen) size
724 ReCreateBitmap(&laser_bitmap, drawto_mm->width, drawto_mm->height);
725 ClearRectangle(laser_bitmap, 0, 0, drawto_mm->width, drawto_mm->height);
729 // set global game control values
730 game_mm.num_cycle = 0;
731 game_mm.num_pacman = 0;
734 game_mm.energy_left = 0; // later set to "native_mm_level.time"
735 game_mm.kettles_still_needed =
736 (native_mm_level.auto_count_kettles ? 0 : native_mm_level.kettles_needed);
737 game_mm.lights_still_needed = 0;
738 game_mm.num_keys = 0;
739 game_mm.ball_choice_pos = 0;
741 game_mm.laser_red = FALSE;
742 game_mm.laser_green = FALSE;
743 game_mm.laser_blue = TRUE;
744 game_mm.has_mcduffin = TRUE;
746 game_mm.level_solved = FALSE;
747 game_mm.game_over = FALSE;
748 game_mm.game_over_cause = 0;
749 game_mm.game_over_message = NULL;
751 game_mm.laser_overload_value = 0;
752 game_mm.laser_enabled = FALSE;
754 // set global laser control values (must be set before "InitLaser()")
755 laser.start_edge.x = 0;
756 laser.start_edge.y = 0;
757 laser.start_angle = 0;
759 for (i = 0; i < MAX_NUM_BEAMERS; i++)
760 laser.beamer[i][0].num = laser.beamer[i][1].num = 0;
762 laser.overloaded = FALSE;
763 laser.overload_value = 0;
764 laser.fuse_off = FALSE;
765 laser.fuse_x = laser.fuse_y = -1;
767 laser.dest_element = EL_EMPTY;
768 laser.dest_element_last = EL_EMPTY;
769 laser.dest_element_last_x = -1;
770 laser.dest_element_last_y = -1;
784 rotate_delay.count = 0;
785 pacman_delay.count = 0;
786 energy_delay.count = 0;
787 overload_delay.count = 0;
789 ClickElement(-1, -1, -1);
791 for (x = 0; x < lev_fieldx; x++)
793 for (y = 0; y < lev_fieldy; y++)
795 Tile[x][y] = Ur[x][y];
796 Hit[x][y] = Box[x][y] = 0;
798 MovPos[x][y] = MovDir[x][y] = MovDelay[x][y] = 0;
799 Store[x][y] = Store2[x][y] = 0;
802 InitField(x, y, TRUE);
809 void InitGameActions_MM(void)
811 int num_init_game_frames = INIT_GAME_ACTIONS_DELAY;
812 int cycle_steps_done = 0;
817 for (i = 0; i <= num_init_game_frames; i++)
819 if (i == num_init_game_frames)
820 StopSound_MM(SND_MM_GAME_LEVELTIME_CHARGING);
821 else if (setup.sound_loops)
822 PlaySoundLoop_MM(SND_MM_GAME_LEVELTIME_CHARGING);
824 PlaySound_MM(SND_MM_GAME_LEVELTIME_CHARGING);
826 game_mm.energy_left = native_mm_level.time * i / num_init_game_frames;
828 UpdateAndDisplayGameControlValues();
830 while (cycle_steps_done < NUM_INIT_CYCLE_STEPS * i / num_init_game_frames)
832 InitCycleElements_RotateSingleStep();
837 AdvanceFrameCounter();
840 if (PendingEscapeKeyEvent())
844 if (setup.quick_doors)
854 if (setup.quick_doors)
860 if (game_mm.kettles_still_needed == 0)
863 SetTileCursorXY(laser.start_edge.x, laser.start_edge.y);
864 SetTileCursorActive(TRUE);
866 // restart all delay counters after initially cycling game elements
867 ResetFrameCounter(&rotate_delay);
868 ResetFrameCounter(&pacman_delay);
869 ResetFrameCounter(&energy_delay);
870 ResetFrameCounter(&overload_delay);
873 static void FadeOutLaser(void)
877 for (i = 15; i >= 0; i--)
879 SetLaserColor(0x11 * i);
881 DrawLaser(0, DL_LASER_ENABLED);
884 Delay_WithScreenUpdates(50);
887 DrawLaser(0, DL_LASER_DISABLED);
889 StopSound_MM(SND_MM_GAME_HEALTH_CHARGING);
892 static void GameOver_MM(int game_over_cause)
894 game_mm.game_over = TRUE;
895 game_mm.game_over_cause = game_over_cause;
896 game_mm.game_over_message = (game_mm.has_mcduffin ?
897 (game_over_cause == GAME_OVER_BOMB ?
898 "Bomb killed Mc Duffin!" :
899 game_over_cause == GAME_OVER_NO_ENERGY ?
900 "Out of magic energy!" :
901 game_over_cause == GAME_OVER_OVERLOADED ?
902 "Magic spell hit Mc Duffin!" :
904 (game_over_cause == GAME_OVER_BOMB ?
905 "Bomb destroyed laser cannon!" :
906 game_over_cause == GAME_OVER_NO_ENERGY ?
907 "Out of laser energy!" :
908 game_over_cause == GAME_OVER_OVERLOADED ?
909 "Laser beam hit laser cannon!" :
912 SetTileCursorActive(FALSE);
915 static void AddLaserEdge(int lx, int ly)
917 int full_sxsize = MAX(FULL_SXSIZE, lev_fieldx * TILEX);
918 int full_sysize = MAX(FULL_SYSIZE, lev_fieldy * TILEY);
920 // check if laser is still inside visible playfield area (or inside level)
921 if (cSX + lx < REAL_SX || cSX + lx >= REAL_SX + full_sxsize ||
922 cSY + ly < REAL_SY || cSY + ly >= REAL_SY + full_sysize)
924 Warn("AddLaserEdge: out of bounds: %d, %d", lx, ly);
929 laser.edge[laser.num_edges].x = cSX2 + lx;
930 laser.edge[laser.num_edges].y = cSY2 + ly;
936 static void AddDamagedField(int ex, int ey)
938 // prevent adding the same field position again
939 if (laser.num_damages > 0 &&
940 laser.damage[laser.num_damages - 1].x == ex &&
941 laser.damage[laser.num_damages - 1].y == ey &&
942 laser.damage[laser.num_damages - 1].edge == laser.num_edges)
945 laser.damage[laser.num_damages].is_mirror = FALSE;
946 laser.damage[laser.num_damages].angle = laser.current_angle;
947 laser.damage[laser.num_damages].edge = laser.num_edges;
948 laser.damage[laser.num_damages].x = ex;
949 laser.damage[laser.num_damages].y = ey;
953 static boolean StepBehind(void)
959 int last_x = laser.edge[laser.num_edges - 1].x - cSX2;
960 int last_y = laser.edge[laser.num_edges - 1].y - cSY2;
962 return ((x - last_x) * XS < 0 || (y - last_y) * YS < 0);
968 static int getMaskFromElement(int element)
970 if (IS_MCDUFFIN(element))
971 return MM_MASK_MCDUFFIN_RIGHT + get_element_phase(element);
972 else if (IS_GRID(element))
973 return MM_MASK_GRID_1 + get_element_phase(element);
974 else if (IS_DF_GRID(element))
975 return MM_MASK_RECTANGLE;
976 else if (IS_DF_SLOPE(element))
977 return MM_MASK_SLOPE_1 + get_element_phase(element);
978 else if (IS_RECTANGLE(element))
979 return MM_MASK_RECTANGLE;
981 return MM_MASK_CIRCLE;
984 static int getLevelFromLaserX(int x)
986 return x / TILEX - (x < 0 ? 1 : 0); // correct negative values
989 static int getLevelFromLaserY(int y)
991 return y / TILEY - (y < 0 ? 1 : 0); // correct negative values
994 static int ScanPixel(void)
999 Debug("game:mm:ScanPixel", "start scanning at (%d, %d) [%d, %d] [%d, %d]",
1000 LX, LY, LX / TILEX, LY / TILEY, LX % TILEX, LY % TILEY);
1003 // follow laser beam until it hits something (at least the screen border)
1004 while (hit_mask == HIT_MASK_NO_HIT)
1010 if (SX + LX < REAL_SX || SX + LX >= REAL_SX + FULL_SXSIZE ||
1011 SY + LY < REAL_SY || SY + LY >= REAL_SY + FULL_SYSIZE)
1013 Debug("game:mm:ScanPixel", "touched screen border!");
1015 return HIT_MASK_ALL;
1019 // check if laser scan has crossed element boundaries (not just mini tiles)
1020 boolean cross_x = (LX / TILEX != (LX + 2) / TILEX);
1021 boolean cross_y = (LY / TILEY != (LY + 2) / TILEY);
1023 if (cross_x && cross_y)
1025 int elx1 = (LX - XS) / TILEX;
1026 int ely1 = (LY + YS) / TILEY;
1027 int elx2 = (LX + XS) / TILEX;
1028 int ely2 = (LY - YS) / TILEY;
1030 // add element corners left and right from the laser beam to damage list
1032 if (IN_LEV_FIELD(elx1, ely1) && Tile[elx1][ely1] != EL_EMPTY)
1033 AddDamagedField(elx1, ely1);
1035 if (IN_LEV_FIELD(elx2, ely2) && Tile[elx2][ely2] != EL_EMPTY)
1036 AddDamagedField(elx2, ely2);
1039 for (i = 0; i < 4; i++)
1041 int px = LX + (i % 2) * 2;
1042 int py = LY + (i / 2) * 2;
1043 int dx = px % TILEX;
1044 int dy = py % TILEY;
1045 int lx = getLevelFromLaserX(px);
1046 int ly = getLevelFromLaserY(py);
1049 if (IN_LEV_FIELD(lx, ly))
1051 int element = Tile[lx][ly];
1053 if (element == EL_EMPTY || element == EL_EXPLODING_TRANSP)
1057 else if (IS_WALL(element) || IS_WALL_CHANGING(element))
1059 int pos = dy / MINI_TILEY * 2 + dx / MINI_TILEX;
1061 pixel = ((element & (1 << pos)) ? 1 : 0);
1065 int pos = getMaskFromElement(element);
1067 pixel = (mm_masks[pos][dy / 2][dx / 2] == 'X' ? 1 : 0);
1072 // check if laser is still inside visible playfield area
1073 pixel = (cSX + px < REAL_SX || cSX + px >= REAL_SX + FULL_SXSIZE ||
1074 cSY + py < REAL_SY || cSY + py >= REAL_SY + FULL_SYSIZE);
1077 if ((Sign[laser.current_angle] & (1 << i)) && pixel)
1078 hit_mask |= (1 << i);
1081 if (hit_mask == HIT_MASK_NO_HIT)
1083 // hit nothing -- go on with another step
1092 static void DeactivateLaserTargetElement(void)
1094 if (laser.dest_element_last == EL_BOMB_ACTIVE ||
1095 laser.dest_element_last == EL_MINE_ACTIVE ||
1096 laser.dest_element_last == EL_GRAY_BALL_ACTIVE ||
1097 laser.dest_element_last == EL_GRAY_BALL_OPENING)
1099 int x = laser.dest_element_last_x;
1100 int y = laser.dest_element_last_y;
1101 int element = laser.dest_element_last;
1103 if (Tile[x][y] == element)
1104 Tile[x][y] = (element == EL_BOMB_ACTIVE ? EL_BOMB :
1105 element == EL_MINE_ACTIVE ? EL_MINE : EL_GRAY_BALL);
1107 if (Tile[x][y] == EL_GRAY_BALL)
1110 laser.dest_element_last = EL_EMPTY;
1111 laser.dest_element_last_x = -1;
1112 laser.dest_element_last_y = -1;
1116 static void ScanLaser(void)
1118 int element = EL_EMPTY;
1119 int last_element = EL_EMPTY;
1120 int end = 0, rf = laser.num_edges;
1122 // do not scan laser again after the game was lost for whatever reason
1123 if (game_mm.game_over)
1126 // do not scan laser if fuse is off
1130 DeactivateLaserTargetElement();
1132 laser.overloaded = FALSE;
1133 laser.stops_inside_element = FALSE;
1135 DrawLaser(0, DL_LASER_ENABLED);
1138 Debug("game:mm:ScanLaser",
1139 "Start scanning with LX == %d, LY == %d, XS == %d, YS == %d",
1147 if (laser.num_edges > MAX_LASER_LEN || laser.num_damages > MAX_LASER_LEN)
1150 laser.overloaded = TRUE;
1155 hit_mask = ScanPixel();
1158 Debug("game:mm:ScanLaser",
1159 "Hit something at LX == %d, LY == %d, XS == %d, YS == %d",
1163 // check if laser scan has hit two diagonally adjacent element corners
1164 boolean diag_1 = ((hit_mask & HIT_MASK_DIAGONAL_1) == HIT_MASK_DIAGONAL_1);
1165 boolean diag_2 = ((hit_mask & HIT_MASK_DIAGONAL_2) == HIT_MASK_DIAGONAL_2);
1167 // check if laser scan has crossed element boundaries (not just mini tiles)
1168 boolean cross_x = (getLevelFromLaserX(LX) != getLevelFromLaserX(LX + 2));
1169 boolean cross_y = (getLevelFromLaserY(LY) != getLevelFromLaserY(LY + 2));
1171 if (cross_x || cross_y)
1173 // hit something at next tile -- check out what it was
1174 ELX = getLevelFromLaserX(LX + XS);
1175 ELY = getLevelFromLaserY(LY + YS);
1179 // hit something at same tile -- check out what it was
1180 ELX = getLevelFromLaserX(LX);
1181 ELY = getLevelFromLaserY(LY);
1185 Debug("game:mm:ScanLaser", "hit_mask (1) == '%x' (%d, %d) (%d, %d)",
1186 hit_mask, LX, LY, ELX, ELY);
1189 if (!IN_LEV_FIELD(ELX, ELY))
1191 // laser next step position
1192 int x = cSX + LX + XS;
1193 int y = cSY + LY + YS;
1195 // check if next step of laser is still inside visible playfield area
1196 if (x >= REAL_SX && x < REAL_SX + FULL_SXSIZE &&
1197 y >= REAL_SY && y < REAL_SY + FULL_SYSIZE)
1199 // go on with another step
1207 laser.dest_element = element;
1212 // handle special case of laser hitting two diagonally adjacent elements
1213 // (with or without a third corner element behind these two elements)
1214 if ((diag_1 || diag_2) && cross_x && cross_y)
1216 // compare the two diagonally adjacent elements
1218 int yoffset = 2 * (diag_1 ? -1 : +1);
1219 int elx1 = (LX - xoffset) / TILEX;
1220 int ely1 = (LY + yoffset) / TILEY;
1221 int elx2 = (LX + xoffset) / TILEX;
1222 int ely2 = (LY - yoffset) / TILEY;
1223 int e1 = Tile[elx1][ely1];
1224 int e2 = Tile[elx2][ely2];
1225 boolean use_element_1 = FALSE;
1227 if (IS_WALL_ICE(e1) || IS_WALL_ICE(e2))
1229 if (IS_WALL_ICE(e1) && IS_WALL_ICE(e2))
1230 use_element_1 = (RND(2) ? TRUE : FALSE);
1231 else if (IS_WALL_ICE(e1))
1232 use_element_1 = TRUE;
1234 else if (IS_WALL_AMOEBA(e1) || IS_WALL_AMOEBA(e2))
1236 // if both tiles match, we can just select the first one
1237 if (IS_WALL_AMOEBA(e1))
1238 use_element_1 = TRUE;
1240 else if (IS_ABSORBING_BLOCK(e1) || IS_ABSORBING_BLOCK(e2))
1242 // if both tiles match, we can just select the first one
1243 if (IS_ABSORBING_BLOCK(e1))
1244 use_element_1 = TRUE;
1247 ELX = (use_element_1 ? elx1 : elx2);
1248 ELY = (use_element_1 ? ely1 : ely2);
1252 Debug("game:mm:ScanLaser", "hit_mask (2) == '%x' (%d, %d) (%d, %d)",
1253 hit_mask, LX, LY, ELX, ELY);
1256 last_element = element;
1258 element = Tile[ELX][ELY];
1259 laser.dest_element = element;
1262 Debug("game:mm:ScanLaser",
1263 "Hit element %d at (%d, %d) [%d, %d] [%d, %d] [%d]",
1266 LX % TILEX, LY % TILEY,
1271 if (!IN_LEV_FIELD(ELX, ELY))
1272 Debug("game:mm:ScanLaser", "WARNING! (1) %d, %d (%d)",
1276 // special case: leaving fixed MM steel grid (upwards) with non-90° angle
1277 if (element == EL_EMPTY &&
1278 IS_GRID_STEEL(last_element) &&
1279 laser.current_angle % 4) // angle is not 90°
1280 element = last_element;
1282 if (element == EL_EMPTY)
1284 if (!HitOnlyAnEdge(hit_mask))
1287 else if (element == EL_FUSE_ON)
1289 if (HitPolarizer(element, hit_mask))
1292 else if (IS_GRID(element) || IS_DF_GRID(element))
1294 if (HitPolarizer(element, hit_mask))
1297 else if (element == EL_BLOCK_STONE || element == EL_BLOCK_WOOD ||
1298 element == EL_GATE_STONE || element == EL_GATE_WOOD)
1300 if (HitBlock(element, hit_mask))
1307 else if (IS_MCDUFFIN(element))
1309 if (HitLaserSource(element, hit_mask))
1312 else if ((element >= EL_EXIT_CLOSED && element <= EL_EXIT_OPEN) ||
1313 IS_RECEIVER(element))
1315 if (HitLaserDestination(element, hit_mask))
1318 else if (IS_WALL(element))
1320 if (IS_WALL_STEEL(element) || IS_DF_WALL_STEEL(element))
1322 if (HitReflectingWalls(element, hit_mask))
1327 if (HitAbsorbingWalls(element, hit_mask))
1333 if (HitElement(element, hit_mask))
1338 DrawLaser(rf - 1, DL_LASER_ENABLED);
1339 rf = laser.num_edges;
1341 if (!IS_DF_WALL_STEEL(element))
1343 // only used for scanning DF steel walls; reset for all other elements
1351 if (laser.dest_element != Tile[ELX][ELY])
1353 Debug("game:mm:ScanLaser",
1354 "ALARM: laser.dest_element == %d, Tile[ELX][ELY] == %d",
1355 laser.dest_element, Tile[ELX][ELY]);
1359 if (!end && !laser.stops_inside_element && !StepBehind())
1362 Debug("game:mm:ScanLaser", "Go one step back");
1368 AddLaserEdge(LX, LY);
1372 DrawLaser(rf - 1, DL_LASER_ENABLED);
1374 Ct = CT = FrameCounter;
1377 if (!IN_LEV_FIELD(ELX, ELY))
1378 Debug("game:mm:ScanLaser", "WARNING! (2) %d, %d", ELX, ELY);
1382 static void ScanLaser_FromLastMirror(void)
1384 int start_pos = (laser.num_damages > 0 ? laser.num_damages - 1 : 0);
1387 for (i = start_pos; i >= 0; i--)
1388 if (laser.damage[i].is_mirror)
1391 int start_edge = (i > 0 ? laser.damage[i].edge - 1 : 0);
1393 DrawLaser(start_edge, DL_LASER_DISABLED);
1398 static void DrawLaserExt(int start_edge, int num_edges, int mode)
1404 Debug("game:mm:DrawLaserExt", "start_edge, num_edges, mode == %d, %d, %d",
1405 start_edge, num_edges, mode);
1410 Warn("DrawLaserExt: start_edge < 0");
1417 Warn("DrawLaserExt: num_edges < 0");
1423 if (mode == DL_LASER_DISABLED)
1425 Debug("game:mm:DrawLaserExt", "Delete laser from edge %d", start_edge);
1429 // now draw the laser to the backbuffer and (if enabled) to the screen
1430 DrawLaserLines(&laser.edge[start_edge], num_edges, mode);
1432 redraw_mask |= REDRAW_FIELD;
1434 if (mode == DL_LASER_ENABLED)
1437 // after the laser was deleted, the "damaged" graphics must be restored
1438 if (laser.num_damages)
1440 int damage_start = 0;
1443 // determine the starting edge, from which graphics need to be restored
1446 for (i = 0; i < laser.num_damages; i++)
1448 if (laser.damage[i].edge == start_edge + 1)
1457 // restore graphics from this starting edge to the end of damage list
1458 for (i = damage_start; i < laser.num_damages; i++)
1460 int lx = laser.damage[i].x;
1461 int ly = laser.damage[i].y;
1462 int element = Tile[lx][ly];
1464 if (Hit[lx][ly] == laser.damage[i].edge)
1465 if (!((IS_BEAMER(element) || IS_FIBRE_OPTIC(element)) &&
1468 if (Box[lx][ly] == laser.damage[i].edge)
1471 if (IS_DRAWABLE(element))
1472 DrawField_MM(lx, ly);
1475 elx = laser.damage[damage_start].x;
1476 ely = laser.damage[damage_start].y;
1477 element = Tile[elx][ely];
1480 if (IS_BEAMER(element))
1484 for (i = 0; i < laser.num_beamers; i++)
1485 Debug("game:mm:DrawLaserExt", "-> %d", laser.beamer_edge[i]);
1487 Debug("game:mm:DrawLaserExt", "IS_BEAMER: [%d]: Hit[%d][%d] == %d [%d]",
1488 mode, elx, ely, Hit[elx][ely], start_edge);
1489 Debug("game:mm:DrawLaserExt", "IS_BEAMER: %d / %d",
1490 get_element_angle(element), laser.damage[damage_start].angle);
1494 if ((IS_BEAMER(element) || IS_FIBRE_OPTIC(element)) &&
1495 laser.num_beamers > 0 &&
1496 start_edge == laser.beamer_edge[laser.num_beamers - 1])
1498 // element is outgoing beamer
1499 laser.num_damages = damage_start + 1;
1501 if (IS_BEAMER(element))
1502 laser.current_angle = get_element_angle(element);
1506 // element is incoming beamer or other element
1507 laser.num_damages = damage_start;
1508 laser.current_angle = laser.damage[laser.num_damages].angle;
1513 // no damages but McDuffin himself (who needs to be redrawn anyway)
1515 elx = laser.start_edge.x;
1516 ely = laser.start_edge.y;
1517 element = Tile[elx][ely];
1520 laser.num_edges = start_edge + 1;
1521 if (start_edge == 0)
1522 laser.current_angle = laser.start_angle;
1524 LX = laser.edge[start_edge].x - cSX2;
1525 LY = laser.edge[start_edge].y - cSY2;
1526 XS = 2 * Step[laser.current_angle].x;
1527 YS = 2 * Step[laser.current_angle].y;
1530 Debug("game:mm:DrawLaserExt", "Set (LX, LY) to (%d, %d) [%d]",
1536 if (IS_BEAMER(element) ||
1537 IS_FIBRE_OPTIC(element) ||
1538 IS_PACMAN(element) ||
1539 IS_POLAR(element) ||
1540 IS_POLAR_CROSS(element) ||
1541 element == EL_FUSE_ON)
1546 Debug("game:mm:DrawLaserExt", "element == %d", element);
1549 if (IS_22_5_ANGLE(laser.current_angle)) // neither 90° nor 45° angle
1550 step_size = ((IS_BEAMER(element) || IS_FIBRE_OPTIC(element)) ? 4 : 3);
1554 if (IS_POLAR(element) || IS_POLAR_CROSS(element) ||
1555 ((IS_BEAMER(element) || IS_FIBRE_OPTIC(element)) &&
1556 (laser.num_beamers == 0 ||
1557 start_edge != laser.beamer_edge[laser.num_beamers - 1])))
1559 // element is incoming beamer or other element
1560 step_size = -step_size;
1565 if (IS_BEAMER(element))
1566 Debug("game:mm:DrawLaserExt",
1567 "start_edge == %d, laser.beamer_edge == %d",
1568 start_edge, laser.beamer_edge);
1571 LX += step_size * XS;
1572 LY += step_size * YS;
1574 else if (element != EL_EMPTY)
1583 Debug("game:mm:DrawLaserExt", "Finally: (LX, LY) to (%d, %d) [%d]",
1588 void DrawLaser(int start_edge, int mode)
1590 // do not draw laser if fuse is off
1591 if (laser.fuse_off && mode == DL_LASER_ENABLED)
1594 if (mode == DL_LASER_DISABLED)
1595 DeactivateLaserTargetElement();
1597 if (laser.num_edges - start_edge < 0)
1599 Warn("DrawLaser: laser.num_edges - start_edge < 0");
1604 // check if laser is interrupted by beamer element
1605 if (laser.num_beamers > 0 &&
1606 start_edge < laser.beamer_edge[laser.num_beamers - 1])
1608 if (mode == DL_LASER_ENABLED)
1611 int tmp_start_edge = start_edge;
1613 // draw laser segments forward from the start to the last beamer
1614 for (i = 0; i < laser.num_beamers; i++)
1616 int tmp_num_edges = laser.beamer_edge[i] - tmp_start_edge;
1618 if (tmp_num_edges <= 0)
1622 Debug("game:mm:DrawLaser", "DL_LASER_ENABLED: i==%d: %d, %d",
1623 i, laser.beamer_edge[i], tmp_start_edge);
1626 DrawLaserExt(tmp_start_edge, tmp_num_edges, DL_LASER_ENABLED);
1628 tmp_start_edge = laser.beamer_edge[i];
1631 // draw last segment from last beamer to the end
1632 DrawLaserExt(tmp_start_edge, laser.num_edges - tmp_start_edge,
1638 int last_num_edges = laser.num_edges;
1639 int num_beamers = laser.num_beamers;
1641 // delete laser segments backward from the end to the first beamer
1642 for (i = num_beamers - 1; i >= 0; i--)
1644 int tmp_num_edges = last_num_edges - laser.beamer_edge[i];
1646 if (laser.beamer_edge[i] - start_edge <= 0)
1649 DrawLaserExt(laser.beamer_edge[i], tmp_num_edges, DL_LASER_DISABLED);
1651 last_num_edges = laser.beamer_edge[i];
1652 laser.num_beamers--;
1656 if (last_num_edges - start_edge <= 0)
1657 Debug("game:mm:DrawLaser", "DL_LASER_DISABLED: %d, %d",
1658 last_num_edges, start_edge);
1661 // special case when rotating first beamer: delete laser edge on beamer
1662 // (but do not start scanning on previous edge to prevent mirror sound)
1663 if (last_num_edges - start_edge == 1 && start_edge > 0)
1664 DrawLaserLines(&laser.edge[start_edge - 1], 2, DL_LASER_DISABLED);
1666 // delete first segment from start to the first beamer
1667 DrawLaserExt(start_edge, last_num_edges - start_edge, DL_LASER_DISABLED);
1672 DrawLaserExt(start_edge, laser.num_edges - start_edge, mode);
1675 game_mm.laser_enabled = mode;
1678 void DrawLaser_MM(void)
1680 DrawLaser(0, game_mm.laser_enabled);
1683 static boolean HitElement(int element, int hit_mask)
1685 if (IS_DF_SLOPE(element))
1687 // check if laser scan has crossed element boundaries (not just mini tiles)
1688 boolean cross_x = (getLevelFromLaserX(LX) != getLevelFromLaserX(LX + 2));
1689 boolean cross_y = (getLevelFromLaserY(LY) != getLevelFromLaserY(LY + 2));
1691 // check if wall (horizontal or vertical) side of slope was hit
1692 if (hit_mask == HIT_MASK_LEFT ||
1693 hit_mask == HIT_MASK_RIGHT ||
1694 hit_mask == HIT_MASK_TOP ||
1695 hit_mask == HIT_MASK_BOTTOM)
1697 return HitReflectingWalls(element, hit_mask);
1700 // check if an edge was hit while crossing element borders
1701 if (cross_x && cross_y && get_number_of_bits(hit_mask) == 1)
1703 // check both sides of potentially diagonal side of slope
1704 int dx1 = (LX + XS) % TILEX;
1705 int dy1 = (LY + YS) % TILEY;
1706 int dx2 = (LX + XS + 2) % TILEX;
1707 int dy2 = (LY + YS + 2) % TILEY;
1708 int pos = getMaskFromElement(element);
1710 // check if we are entering empty space area after hitting edge
1711 if (mm_masks[pos][dy1 / 2][dx1 / 2] != 'X' &&
1712 mm_masks[pos][dy2 / 2][dx2 / 2] != 'X')
1714 // we already know that we hit an edge, but use this function to go on
1715 if (HitOnlyAnEdge(hit_mask))
1720 int mirrored_angle = get_mirrored_angle(laser.current_angle,
1721 get_element_angle(element));
1722 int opposite_angle = get_opposite_angle(laser.current_angle);
1724 // check if laser is reflected by slope by 180°
1725 if (mirrored_angle == opposite_angle)
1727 AddDamagedField(LX / TILEX, LY / TILEY);
1729 laser.overloaded = TRUE;
1736 if (HitOnlyAnEdge(hit_mask))
1740 if (IS_MOVING(ELX, ELY) || IS_BLOCKED(ELX, ELY))
1741 element = MovingOrBlocked2Element_MM(ELX, ELY);
1744 Debug("game:mm:HitElement", "(1): element == %d", element);
1748 if ((ELX * TILEX + 14 - LX) * YS == (ELY * TILEY + 14 - LY) * XS)
1749 Debug("game:mm:HitElement", "(%d): EXACT MATCH @ (%d, %d)",
1752 Debug("game:mm:HitElement", "(%d): FUZZY MATCH @ (%d, %d)",
1756 AddDamagedField(ELX, ELY);
1758 boolean through_center = ((ELX * TILEX + 14 - LX) * YS ==
1759 (ELY * TILEY + 14 - LY) * XS);
1761 // this is more precise: check if laser would go through the center
1762 if (!IS_DF_SLOPE(element) && !through_center)
1766 // prevent cutting through laser emitter with laser beam
1767 if (IS_LASER(element))
1770 // skip the whole element before continuing the scan
1778 while (ELX == LX/TILEX && ELY == LY/TILEY && LX > 0 && LY > 0);
1780 if ((LX/TILEX > ELX || LY/TILEY > ELY) && skip_count > 1)
1782 /* skipping scan positions to the right and down skips one scan
1783 position too much, because this is only the top left scan position
1784 of totally four scan positions (plus one to the right, one to the
1785 bottom and one to the bottom right) */
1786 /* ... but only roll back scan position if more than one step done */
1796 Debug("game:mm:HitElement", "(2): element == %d", element);
1799 if (LX + 5 * XS < 0 ||
1809 Debug("game:mm:HitElement", "(3): element == %d", element);
1812 if (IS_POLAR(element) &&
1813 ((element - EL_POLAR_START) % 2 ||
1814 (element - EL_POLAR_START) / 2 != laser.current_angle % 8))
1816 PlayLevelSound_MM(ELX, ELY, element, MM_ACTION_HITTING);
1818 laser.num_damages--;
1823 if (IS_POLAR_CROSS(element) &&
1824 (element - EL_POLAR_CROSS_START) != laser.current_angle % 4)
1826 PlayLevelSound_MM(ELX, ELY, element, MM_ACTION_HITTING);
1828 laser.num_damages--;
1833 if (IS_DF_SLOPE(element) && !through_center)
1837 if (hit_mask == HIT_MASK_ALL)
1839 // laser already inside slope -- go back half step
1846 AddLaserEdge(LX, LY);
1848 LX -= (ABS(XS) < ABS(YS) ? correction * SIGN(XS) : 0);
1849 LY -= (ABS(YS) < ABS(XS) ? correction * SIGN(YS) : 0);
1851 else if (!IS_BEAMER(element) &&
1852 !IS_FIBRE_OPTIC(element) &&
1853 !IS_GRID_WOOD(element) &&
1854 element != EL_FUEL_EMPTY)
1857 if ((ELX * TILEX + 14 - LX) * YS == (ELY * TILEY + 14 - LY) * XS)
1858 Debug("game:mm:HitElement", "EXACT MATCH @ (%d, %d)", ELX, ELY);
1860 Debug("game:mm:HitElement", "FUZZY MATCH @ (%d, %d)", ELX, ELY);
1863 LX = ELX * TILEX + 14;
1864 LY = ELY * TILEY + 14;
1866 AddLaserEdge(LX, LY);
1869 if (IS_MIRROR(element) ||
1870 IS_MIRROR_FIXED(element) ||
1871 IS_POLAR(element) ||
1872 IS_POLAR_CROSS(element) ||
1873 IS_DF_MIRROR(element) ||
1874 IS_DF_MIRROR_AUTO(element) ||
1875 IS_DF_MIRROR_FIXED(element) ||
1876 IS_DF_SLOPE(element) ||
1877 element == EL_PRISM ||
1878 element == EL_REFRACTOR)
1880 int current_angle = laser.current_angle;
1883 laser.num_damages--;
1885 AddDamagedField(ELX, ELY);
1887 laser.damage[laser.num_damages - 1].is_mirror = TRUE;
1890 Hit[ELX][ELY] = laser.damage[laser.num_damages - 1].edge;
1892 if (IS_MIRROR(element) ||
1893 IS_MIRROR_FIXED(element) ||
1894 IS_DF_MIRROR(element) ||
1895 IS_DF_MIRROR_AUTO(element) ||
1896 IS_DF_MIRROR_FIXED(element) ||
1897 IS_DF_SLOPE(element))
1898 laser.current_angle = get_mirrored_angle(laser.current_angle,
1899 get_element_angle(element));
1901 if (element == EL_PRISM || element == EL_REFRACTOR)
1902 laser.current_angle = RND(16);
1904 XS = 2 * Step[laser.current_angle].x;
1905 YS = 2 * Step[laser.current_angle].y;
1909 // start from center position for all game elements but slope
1910 if (!IS_22_5_ANGLE(laser.current_angle)) // 90° or 45° angle
1915 LX += step_size * XS;
1916 LY += step_size * YS;
1920 // advance laser position until reaching the next tile (slopes)
1921 while (LX / TILEX == ELX && (LX + 2) / TILEX == ELX &&
1922 LY / TILEY == ELY && (LY + 2) / TILEY == ELY)
1929 // draw sparkles on mirror
1930 if ((IS_MIRROR(element) ||
1931 IS_MIRROR_FIXED(element) ||
1932 element == EL_PRISM) &&
1933 current_angle != laser.current_angle)
1935 MovDelay[ELX][ELY] = 11; // start animation
1938 if ((!IS_POLAR(element) && !IS_POLAR_CROSS(element)) &&
1939 current_angle != laser.current_angle)
1940 PlayLevelSound_MM(ELX, ELY, element, MM_ACTION_HITTING);
1943 (get_opposite_angle(laser.current_angle) ==
1944 laser.damage[laser.num_damages - 1].angle ? TRUE : FALSE);
1946 if (IS_DF_SLOPE(element))
1948 // handle special cases for slope element
1950 if (IS_45_ANGLE(laser.current_angle))
1954 elx = getLevelFromLaserX(LX);
1955 ely = getLevelFromLaserY(LY);
1957 if (IN_LEV_FIELD(elx, ely))
1959 int element_next = Tile[elx][ely];
1961 // check if slope is followed by slope with opposite orientation
1962 if (IS_DF_SLOPE(element_next) && ABS(element - element_next) == 2)
1963 laser.overloaded = TRUE;
1966 int nr = element - EL_DF_SLOPE_START;
1967 int dx = (nr == 0 ? (XS > 0 ? TILEX - 1 : -1) :
1968 nr == 1 ? (XS > 0 ? TILEX : 1) :
1969 nr == 2 ? (XS > 0 ? TILEX : 1) :
1970 nr == 3 ? (XS > 0 ? TILEX - 1 : -1) : 0);
1971 int dy = (nr == 0 ? (YS > 0 ? TILEY - 1 : -1) :
1972 nr == 1 ? (YS > 0 ? TILEY - 1 : -1) :
1973 nr == 2 ? (YS > 0 ? TILEY : 0) :
1974 nr == 3 ? (YS > 0 ? TILEY : 0) : 0);
1976 int px = ELX * TILEX + dx;
1977 int py = ELY * TILEY + dy;
1982 elx = getLevelFromLaserX(px);
1983 ely = getLevelFromLaserY(py);
1985 if (IN_LEV_FIELD(elx, ely))
1987 int element_side = Tile[elx][ely];
1989 // check if end of slope is blocked by other element
1990 if (IS_WALL(element_side) || IS_WALL_CHANGING(element_side))
1992 int pos = dy / MINI_TILEY * 2 + dx / MINI_TILEX;
1994 if (element & (1 << pos))
1995 laser.overloaded = TRUE;
1999 int pos = getMaskFromElement(element_side);
2001 if (mm_masks[pos][dy / 2][dx / 2] == 'X')
2002 laser.overloaded = TRUE;
2008 return (laser.overloaded ? TRUE : FALSE);
2011 if (element == EL_FUEL_FULL)
2013 laser.stops_inside_element = TRUE;
2018 if (element == EL_BOMB || element == EL_MINE || element == EL_GRAY_BALL)
2020 PlayLevelSound_MM(ELX, ELY, element, MM_ACTION_HITTING);
2022 Tile[ELX][ELY] = (element == EL_BOMB ? EL_BOMB_ACTIVE :
2023 element == EL_MINE ? EL_MINE_ACTIVE :
2024 EL_GRAY_BALL_ACTIVE);
2026 GfxFrame[ELX][ELY] = 0; // restart animation
2028 laser.dest_element_last = Tile[ELX][ELY];
2029 laser.dest_element_last_x = ELX;
2030 laser.dest_element_last_y = ELY;
2032 if (element == EL_MINE)
2033 laser.overloaded = TRUE;
2036 if (element == EL_KETTLE ||
2037 element == EL_CELL ||
2038 element == EL_KEY ||
2039 element == EL_LIGHTBALL ||
2040 element == EL_PACMAN ||
2041 IS_PACMAN(element) ||
2042 IS_ENVELOPE(element))
2044 if (!IS_PACMAN(element) &&
2045 !IS_ENVELOPE(element))
2048 if (element == EL_PACMAN)
2051 if (element == EL_KETTLE || element == EL_CELL)
2053 if (game_mm.kettles_still_needed > 0)
2054 game_mm.kettles_still_needed--;
2056 game.snapshot.collected_item = TRUE;
2058 if (game_mm.kettles_still_needed == 0)
2062 DrawLaser(0, DL_LASER_ENABLED);
2065 else if (element == EL_KEY)
2069 else if (IS_PACMAN(element))
2071 DeletePacMan(ELX, ELY);
2073 else if (IS_ENVELOPE(element))
2075 Tile[ELX][ELY] = EL_ENVELOPE_1_OPENING + ENVELOPE_NR(Tile[ELX][ELY]);
2078 RaiseScoreElement_MM(element);
2083 if (element == EL_LIGHTBULB_OFF || element == EL_LIGHTBULB_ON)
2085 PlayLevelSound_MM(ELX, ELY, element, MM_ACTION_HITTING);
2087 DrawLaser(0, DL_LASER_ENABLED);
2089 if (Tile[ELX][ELY] == EL_LIGHTBULB_OFF)
2091 Tile[ELX][ELY] = EL_LIGHTBULB_ON;
2092 game_mm.lights_still_needed--;
2096 Tile[ELX][ELY] = EL_LIGHTBULB_OFF;
2097 game_mm.lights_still_needed++;
2100 DrawField_MM(ELX, ELY);
2101 DrawLaser(0, DL_LASER_ENABLED);
2106 laser.stops_inside_element = TRUE;
2112 Debug("game:mm:HitElement", "(4): element == %d", element);
2115 if ((IS_BEAMER(element) || IS_FIBRE_OPTIC(element)) &&
2116 laser.num_beamers < MAX_NUM_BEAMERS &&
2117 laser.beamer[BEAMER_NR(element)][1].num)
2119 int beamer_angle = get_element_angle(element);
2120 int beamer_nr = BEAMER_NR(element);
2124 Debug("game:mm:HitElement", "(BEAMER): element == %d", element);
2127 laser.num_damages--;
2129 if (IS_FIBRE_OPTIC(element) ||
2130 laser.current_angle == get_opposite_angle(beamer_angle))
2134 LX = ELX * TILEX + 14;
2135 LY = ELY * TILEY + 14;
2137 AddLaserEdge(LX, LY);
2138 AddDamagedField(ELX, ELY);
2140 laser.damage[laser.num_damages - 1].is_mirror = TRUE;
2143 Hit[ELX][ELY] = laser.damage[laser.num_damages - 1].edge;
2145 pos = (ELX == laser.beamer[beamer_nr][0].x &&
2146 ELY == laser.beamer[beamer_nr][0].y ? 1 : 0);
2147 ELX = laser.beamer[beamer_nr][pos].x;
2148 ELY = laser.beamer[beamer_nr][pos].y;
2149 LX = ELX * TILEX + 14;
2150 LY = ELY * TILEY + 14;
2152 if (IS_BEAMER(element))
2154 laser.current_angle = get_element_angle(Tile[ELX][ELY]);
2155 XS = 2 * Step[laser.current_angle].x;
2156 YS = 2 * Step[laser.current_angle].y;
2159 laser.beamer_edge[laser.num_beamers] = laser.num_edges;
2161 AddLaserEdge(LX, LY);
2162 AddDamagedField(ELX, ELY);
2164 laser.damage[laser.num_damages - 1].is_mirror = TRUE;
2167 Hit[ELX][ELY] = laser.damage[laser.num_damages - 1].edge;
2169 if (laser.current_angle == (laser.current_angle >> 1) << 1)
2174 LX += step_size * XS;
2175 LY += step_size * YS;
2177 laser.num_beamers++;
2186 static boolean HitOnlyAnEdge(int hit_mask)
2188 // check if the laser hit only the edge of an element and, if so, go on
2191 Debug("game:mm:HitOnlyAnEdge", "LX, LY, hit_mask == %d, %d, %d",
2195 if ((hit_mask == HIT_MASK_TOPLEFT ||
2196 hit_mask == HIT_MASK_TOPRIGHT ||
2197 hit_mask == HIT_MASK_BOTTOMLEFT ||
2198 hit_mask == HIT_MASK_BOTTOMRIGHT) &&
2199 laser.current_angle % 4) // angle is not 90°
2203 if (hit_mask == HIT_MASK_TOPLEFT)
2208 else if (hit_mask == HIT_MASK_TOPRIGHT)
2213 else if (hit_mask == HIT_MASK_BOTTOMLEFT)
2218 else // (hit_mask == HIT_MASK_BOTTOMRIGHT)
2224 AddDamagedField((LX + 2 * dx) / TILEX, (LY + 2 * dy) / TILEY);
2230 Debug("game:mm:HitOnlyAnEdge", "[HitOnlyAnEdge() == TRUE]");
2237 Debug("game:mm:HitOnlyAnEdge", "[HitOnlyAnEdge() == FALSE]");
2243 static boolean HitPolarizer(int element, int hit_mask)
2245 if (HitOnlyAnEdge(hit_mask))
2248 if (IS_DF_GRID(element))
2250 int grid_angle = get_element_angle(element);
2253 Debug("game:mm:HitPolarizer", "angle: grid == %d, laser == %d",
2254 grid_angle, laser.current_angle);
2257 AddLaserEdge(LX, LY);
2258 AddDamagedField(ELX, ELY);
2261 Hit[ELX][ELY] = laser.damage[laser.num_damages - 1].edge;
2263 if (laser.current_angle == grid_angle ||
2264 laser.current_angle == get_opposite_angle(grid_angle))
2266 // skip the whole element before continuing the scan
2272 while (ELX == LX/TILEX && ELY == LY/TILEY && LX > 0 && LY > 0);
2274 if (LX/TILEX > ELX || LY/TILEY > ELY)
2276 /* skipping scan positions to the right and down skips one scan
2277 position too much, because this is only the top left scan position
2278 of totally four scan positions (plus one to the right, one to the
2279 bottom and one to the bottom right) */
2285 AddLaserEdge(LX, LY);
2291 Debug("game:mm:HitPolarizer", "LX, LY == %d, %d [%d, %d] [%d, %d]",
2293 LX / TILEX, LY / TILEY,
2294 LX % TILEX, LY % TILEY);
2299 else if (IS_GRID_STEEL_FIXED(element) || IS_GRID_STEEL_AUTO(element))
2301 return HitReflectingWalls(element, hit_mask);
2305 return HitAbsorbingWalls(element, hit_mask);
2308 else if (IS_GRID_STEEL(element))
2310 // may be required if graphics for steel grid redefined
2311 AddDamagedField(ELX, ELY);
2313 return HitReflectingWalls(element, hit_mask);
2315 else // IS_GRID_WOOD
2317 // may be required if graphics for wooden grid redefined
2318 AddDamagedField(ELX, ELY);
2320 return HitAbsorbingWalls(element, hit_mask);
2326 static boolean HitBlock(int element, int hit_mask)
2328 boolean check = FALSE;
2330 if ((element == EL_GATE_STONE || element == EL_GATE_WOOD) &&
2331 game_mm.num_keys == 0)
2334 if (element == EL_BLOCK_STONE || element == EL_BLOCK_WOOD)
2337 int ex = ELX * TILEX + 14;
2338 int ey = ELY * TILEY + 14;
2342 for (i = 1; i < 32; i++)
2347 if ((x == ex || x == ex + 1) && (y == ey || y == ey + 1))
2352 if (check && (element == EL_BLOCK_WOOD || element == EL_GATE_WOOD))
2353 return HitAbsorbingWalls(element, hit_mask);
2357 AddLaserEdge(LX - XS, LY - YS);
2358 AddDamagedField(ELX, ELY);
2361 Box[ELX][ELY] = laser.num_edges;
2363 return HitReflectingWalls(element, hit_mask);
2366 if (element == EL_GATE_STONE || element == EL_GATE_WOOD)
2368 int xs = XS / 2, ys = YS / 2;
2370 if ((hit_mask & HIT_MASK_DIAGONAL_1) == HIT_MASK_DIAGONAL_1 ||
2371 (hit_mask & HIT_MASK_DIAGONAL_2) == HIT_MASK_DIAGONAL_2)
2373 laser.overloaded = (element == EL_GATE_STONE);
2378 if (ABS(xs) == 1 && ABS(ys) == 1 &&
2379 (hit_mask == HIT_MASK_TOP ||
2380 hit_mask == HIT_MASK_LEFT ||
2381 hit_mask == HIT_MASK_RIGHT ||
2382 hit_mask == HIT_MASK_BOTTOM))
2383 AddDamagedField(ELX - xs * (hit_mask == HIT_MASK_TOP ||
2384 hit_mask == HIT_MASK_BOTTOM),
2385 ELY - ys * (hit_mask == HIT_MASK_LEFT ||
2386 hit_mask == HIT_MASK_RIGHT));
2387 AddLaserEdge(LX, LY);
2393 if (element == EL_GATE_STONE && Box[ELX][ELY])
2395 DrawLaser(Box[ELX][ELY] - 1, DL_LASER_DISABLED);
2407 if (element == EL_BLOCK_STONE || element == EL_BLOCK_WOOD)
2409 int xs = XS / 2, ys = YS / 2;
2411 if ((hit_mask & HIT_MASK_DIAGONAL_1) == HIT_MASK_DIAGONAL_1 ||
2412 (hit_mask & HIT_MASK_DIAGONAL_2) == HIT_MASK_DIAGONAL_2)
2414 laser.overloaded = (element == EL_BLOCK_STONE);
2419 if (ABS(xs) == 1 && ABS(ys) == 1 &&
2420 (hit_mask == HIT_MASK_TOP ||
2421 hit_mask == HIT_MASK_LEFT ||
2422 hit_mask == HIT_MASK_RIGHT ||
2423 hit_mask == HIT_MASK_BOTTOM))
2424 AddDamagedField(ELX - xs * (hit_mask == HIT_MASK_TOP ||
2425 hit_mask == HIT_MASK_BOTTOM),
2426 ELY - ys * (hit_mask == HIT_MASK_LEFT ||
2427 hit_mask == HIT_MASK_RIGHT));
2428 AddDamagedField(ELX, ELY);
2430 LX = ELX * TILEX + 14;
2431 LY = ELY * TILEY + 14;
2433 AddLaserEdge(LX, LY);
2435 laser.stops_inside_element = TRUE;
2443 static boolean HitLaserSource(int element, int hit_mask)
2445 if (HitOnlyAnEdge(hit_mask))
2448 PlayLevelSound_MM(ELX, ELY, element, MM_ACTION_HITTING);
2450 laser.overloaded = TRUE;
2455 static boolean HitLaserDestination(int element, int hit_mask)
2457 if (HitOnlyAnEdge(hit_mask))
2460 if (element != EL_EXIT_OPEN &&
2461 !(IS_RECEIVER(element) &&
2462 game_mm.kettles_still_needed == 0 &&
2463 laser.current_angle == get_opposite_angle(get_element_angle(element))))
2465 PlayLevelSound_MM(ELX, ELY, element, MM_ACTION_HITTING);
2470 if (IS_RECEIVER(element) ||
2471 (IS_22_5_ANGLE(laser.current_angle) &&
2472 (ELX != (LX + 6 * XS) / TILEX ||
2473 ELY != (LY + 6 * YS) / TILEY ||
2482 LX = ELX * TILEX + 14;
2483 LY = ELY * TILEY + 14;
2485 laser.stops_inside_element = TRUE;
2488 AddLaserEdge(LX, LY);
2489 AddDamagedField(ELX, ELY);
2491 if (game_mm.lights_still_needed == 0)
2493 game_mm.level_solved = TRUE;
2495 SetTileCursorActive(FALSE);
2501 static boolean HitReflectingWalls(int element, int hit_mask)
2503 // check if laser hits side of a wall with an angle that is not 90°
2504 if (!IS_90_ANGLE(laser.current_angle) && (hit_mask == HIT_MASK_TOP ||
2505 hit_mask == HIT_MASK_LEFT ||
2506 hit_mask == HIT_MASK_RIGHT ||
2507 hit_mask == HIT_MASK_BOTTOM))
2509 PlayLevelSound_MM(ELX, ELY, element, MM_ACTION_HITTING);
2514 if (!IS_DF_GRID(element))
2515 AddLaserEdge(LX, LY);
2517 // check if laser hits wall with an angle of 45°
2518 if (!IS_22_5_ANGLE(laser.current_angle))
2520 if (hit_mask == HIT_MASK_TOP || hit_mask == HIT_MASK_BOTTOM)
2523 laser.current_angle = get_mirrored_angle(laser.current_angle,
2526 else // hit_mask == HIT_MASK_LEFT || hit_mask == HIT_MASK_RIGHT
2529 laser.current_angle = get_mirrored_angle(laser.current_angle,
2533 AddLaserEdge(LX, LY);
2535 XS = 2 * Step[laser.current_angle].x;
2536 YS = 2 * Step[laser.current_angle].y;
2540 else if (hit_mask == HIT_MASK_TOP || hit_mask == HIT_MASK_BOTTOM)
2542 laser.current_angle = get_mirrored_angle(laser.current_angle,
2547 if (!IS_DF_GRID(element))
2548 AddLaserEdge(LX, LY);
2553 if (!IS_DF_GRID(element))
2554 AddLaserEdge(LX, LY + YS / 2);
2557 if (!IS_DF_GRID(element))
2558 AddLaserEdge(LX, LY);
2561 YS = 2 * Step[laser.current_angle].y;
2565 else // hit_mask == HIT_MASK_LEFT || hit_mask == HIT_MASK_RIGHT
2567 laser.current_angle = get_mirrored_angle(laser.current_angle,
2572 if (!IS_DF_GRID(element))
2573 AddLaserEdge(LX, LY);
2578 if (!IS_DF_GRID(element))
2579 AddLaserEdge(LX + XS / 2, LY);
2582 if (!IS_DF_GRID(element))
2583 AddLaserEdge(LX, LY);
2586 XS = 2 * Step[laser.current_angle].x;
2592 // reflection at the edge of reflecting DF style wall
2593 if (IS_DF_WALL_STEEL(element) && IS_22_5_ANGLE(laser.current_angle))
2595 if (((laser.current_angle == 1 || laser.current_angle == 3) &&
2596 hit_mask == HIT_MASK_TOPRIGHT) ||
2597 ((laser.current_angle == 5 || laser.current_angle == 7) &&
2598 hit_mask == HIT_MASK_TOPLEFT) ||
2599 ((laser.current_angle == 9 || laser.current_angle == 11) &&
2600 hit_mask == HIT_MASK_BOTTOMLEFT) ||
2601 ((laser.current_angle == 13 || laser.current_angle == 15) &&
2602 hit_mask == HIT_MASK_BOTTOMRIGHT))
2605 (hit_mask == HIT_MASK_TOPRIGHT || hit_mask == HIT_MASK_BOTTOMLEFT ?
2606 ANG_MIRROR_135 : ANG_MIRROR_45);
2608 PlayLevelSound_MM(ELX, ELY, element, MM_ACTION_HITTING);
2610 AddDamagedField(ELX, ELY);
2611 AddLaserEdge(LX, LY);
2613 laser.current_angle = get_mirrored_angle(laser.current_angle,
2621 AddLaserEdge(LX, LY);
2627 // reflection inside an edge of reflecting DF style wall
2628 if (IS_DF_WALL_STEEL(element) && IS_22_5_ANGLE(laser.current_angle))
2630 if (((laser.current_angle == 1 || laser.current_angle == 3) &&
2631 hit_mask == (HIT_MASK_ALL ^ HIT_MASK_BOTTOMLEFT)) ||
2632 ((laser.current_angle == 5 || laser.current_angle == 7) &&
2633 hit_mask == (HIT_MASK_ALL ^ HIT_MASK_BOTTOMRIGHT)) ||
2634 ((laser.current_angle == 9 || laser.current_angle == 11) &&
2635 hit_mask == (HIT_MASK_ALL ^ HIT_MASK_TOPRIGHT)) ||
2636 ((laser.current_angle == 13 || laser.current_angle == 15) &&
2637 hit_mask == (HIT_MASK_ALL ^ HIT_MASK_TOPLEFT)))
2640 (hit_mask == (HIT_MASK_ALL ^ HIT_MASK_BOTTOMLEFT) ||
2641 hit_mask == (HIT_MASK_ALL ^ HIT_MASK_TOPRIGHT) ?
2642 ANG_MIRROR_135 : ANG_MIRROR_45);
2644 PlayLevelSound_MM(ELX, ELY, element, MM_ACTION_HITTING);
2647 AddDamagedField(ELX, ELY);
2650 AddLaserEdge(LX - XS, LY - YS);
2651 AddLaserEdge(LX - XS + (ABS(XS) == 4 ? XS/2 : 0),
2652 LY - YS + (ABS(YS) == 4 ? YS/2 : 0));
2654 laser.current_angle = get_mirrored_angle(laser.current_angle,
2662 AddLaserEdge(LX, LY);
2668 // check if laser hits DF style wall with an angle of 90°
2669 if (IS_DF_WALL(element) && IS_90_ANGLE(laser.current_angle))
2671 if ((IS_HORIZ_ANGLE(laser.current_angle) &&
2672 (!(hit_mask & HIT_MASK_TOP) || !(hit_mask & HIT_MASK_BOTTOM))) ||
2673 (IS_VERT_ANGLE(laser.current_angle) &&
2674 (!(hit_mask & HIT_MASK_LEFT) || !(hit_mask & HIT_MASK_RIGHT))))
2676 // laser at last step touched nothing or the same side of the wall
2677 if (LX != last_LX || LY != last_LY || hit_mask == last_hit_mask)
2679 AddDamagedField(ELX, ELY);
2686 last_hit_mask = hit_mask;
2693 if (!HitOnlyAnEdge(hit_mask))
2695 laser.overloaded = TRUE;
2703 static boolean HitAbsorbingWalls(int element, int hit_mask)
2705 if (HitOnlyAnEdge(hit_mask))
2709 (hit_mask == HIT_MASK_LEFT || hit_mask == HIT_MASK_RIGHT))
2711 AddLaserEdge(LX - XS, LY - YS);
2718 (hit_mask == HIT_MASK_TOP || hit_mask == HIT_MASK_BOTTOM))
2720 AddLaserEdge(LX - XS, LY - YS);
2726 if (IS_WALL_WOOD(element) ||
2727 IS_DF_WALL_WOOD(element) ||
2728 IS_GRID_WOOD(element) ||
2729 IS_GRID_WOOD_FIXED(element) ||
2730 IS_GRID_WOOD_AUTO(element) ||
2731 element == EL_FUSE_ON ||
2732 element == EL_BLOCK_WOOD ||
2733 element == EL_GATE_WOOD)
2735 PlayLevelSound_MM(ELX, ELY, element, MM_ACTION_HITTING);
2740 if (IS_WALL_ICE(element))
2746 // check if laser hit adjacent edges of two diagonal tiles
2747 if (ELX != lx / TILEX)
2749 if (ELY != ly / TILEY)
2752 mask = lx / MINI_TILEX - ELX * 2 + 1; // Quadrant (horizontal)
2753 mask <<= ((ly / MINI_TILEY - ELY * 2) > 0 ? 2 : 0); // || (vertical)
2755 // check if laser hits wall with an angle of 90°
2756 if (IS_90_ANGLE(laser.current_angle))
2757 mask += mask * (2 + IS_HORIZ_ANGLE(laser.current_angle) * 2);
2759 if (mask == 1 || mask == 2 || mask == 4 || mask == 8)
2763 for (i = 0; i < 4; i++)
2765 if (mask == (1 << i) && (XS > 0) == (i % 2) && (YS > 0) == (i / 2))
2766 mask = 15 - (8 >> i);
2767 else if (ABS(XS) == 4 &&
2769 (XS > 0) == (i % 2) &&
2770 (YS < 0) == (i / 2))
2771 mask = 3 + (i / 2) * 9;
2772 else if (ABS(YS) == 4 &&
2774 (XS < 0) == (i % 2) &&
2775 (YS > 0) == (i / 2))
2776 mask = 5 + (i % 2) * 5;
2780 laser.wall_mask = mask;
2782 else if (IS_WALL_AMOEBA(element))
2784 int elx = (LX - 2 * XS) / TILEX;
2785 int ely = (LY - 2 * YS) / TILEY;
2786 int element2 = Tile[elx][ely];
2789 if (element2 != EL_EMPTY && !IS_WALL_AMOEBA(element2))
2791 laser.dest_element = EL_EMPTY;
2799 mask = (LX - 2 * XS) / 16 - ELX * 2 + 1;
2800 mask <<= ((LY - 2 * YS) / 16 - ELY * 2) * 2;
2802 if (IS_90_ANGLE(laser.current_angle))
2803 mask += mask * (2 + IS_HORIZ_ANGLE(laser.current_angle) * 2);
2805 laser.dest_element = element2 | EL_WALL_AMOEBA_BASE;
2807 laser.wall_mask = mask;
2813 static void OpenExit(int x, int y)
2817 if (!MovDelay[x][y]) // next animation frame
2818 MovDelay[x][y] = 4 * delay;
2820 if (MovDelay[x][y]) // wait some time before next frame
2825 phase = MovDelay[x][y] / delay;
2827 if (!(MovDelay[x][y] % delay) && IN_SCR_FIELD(x, y))
2828 DrawGraphicAnimation_MM(x, y, IMG_MM_EXIT_OPENING, 3 - phase);
2830 if (!MovDelay[x][y])
2832 Tile[x][y] = EL_EXIT_OPEN;
2838 static void OpenGrayBall(int x, int y)
2842 if (!MovDelay[x][y]) // next animation frame
2844 if (IS_WALL(Store[x][y]))
2846 DrawWalls_MM(x, y, Store[x][y]);
2848 // copy wall tile to spare bitmap for "melting" animation
2849 BlitBitmap(drawto_mm, bitmap_db_field, cSX + x * TILEX, cSY + y * TILEY,
2850 TILEX, TILEY, x * TILEX, y * TILEY);
2852 DrawElement_MM(x, y, EL_GRAY_BALL);
2855 MovDelay[x][y] = 50 * delay;
2858 if (MovDelay[x][y]) // wait some time before next frame
2862 if (!(MovDelay[x][y] % delay) && IN_SCR_FIELD(x, y))
2866 int dx = RND(26), dy = RND(26);
2868 if (IS_WALL(Store[x][y]))
2870 // copy wall tile from spare bitmap for "melting" animation
2871 bitmap = bitmap_db_field;
2877 int graphic = el2gfx(Store[x][y]);
2879 getGraphicSource(graphic, 0, &bitmap, &gx, &gy);
2882 BlitBitmap(bitmap, drawto_mm, gx + dx, gy + dy, 6, 6,
2883 cSX + x * TILEX + dx, cSY + y * TILEY + dy);
2885 laser.redraw = TRUE;
2887 MarkTileDirty(x, y);
2890 if (!MovDelay[x][y])
2892 Tile[x][y] = Store[x][y];
2893 Store[x][y] = Store2[x][y] = 0;
2894 MovDir[x][y] = MovPos[x][y] = MovDelay[x][y] = 0;
2896 InitField(x, y, FALSE);
2899 ScanLaser_FromLastMirror();
2904 static void OpenEnvelope(int x, int y)
2906 int num_frames = 8; // seven frames plus final empty space
2908 if (!MovDelay[x][y]) // next animation frame
2909 MovDelay[x][y] = num_frames;
2911 if (MovDelay[x][y]) // wait some time before next frame
2913 int nr = ENVELOPE_OPENING_NR(Tile[x][y]);
2917 if (MovDelay[x][y] > 0 && IN_SCR_FIELD(x, y))
2919 int graphic = el_act2gfx(EL_ENVELOPE_1 + nr, MM_ACTION_COLLECTING);
2920 int frame = num_frames - MovDelay[x][y] - 1;
2922 DrawGraphicAnimation_MM(x, y, graphic, frame);
2924 laser.redraw = TRUE;
2927 if (MovDelay[x][y] == 0)
2929 Tile[x][y] = EL_EMPTY;
2940 static void MeltIce(int x, int y)
2945 if (!MovDelay[x][y]) // next animation frame
2946 MovDelay[x][y] = frames * delay;
2948 if (MovDelay[x][y]) // wait some time before next frame
2951 int wall_mask = Store2[x][y];
2952 int real_element = Tile[x][y] - EL_WALL_CHANGING_BASE + EL_WALL_ICE_BASE;
2955 phase = frames - MovDelay[x][y] / delay - 1;
2957 if (!MovDelay[x][y])
2959 Tile[x][y] = real_element & (wall_mask ^ 0xFF);
2960 Store[x][y] = Store2[x][y] = 0;
2962 DrawWalls_MM(x, y, Tile[x][y]);
2964 if (Tile[x][y] == EL_WALL_ICE_BASE)
2965 Tile[x][y] = EL_EMPTY;
2967 ScanLaser_FromLastMirror();
2969 else if (!(MovDelay[x][y] % delay) && IN_SCR_FIELD(x, y))
2971 DrawWallsAnimation_MM(x, y, real_element, phase, wall_mask);
2973 laser.redraw = TRUE;
2978 static void GrowAmoeba(int x, int y)
2983 if (!MovDelay[x][y]) // next animation frame
2984 MovDelay[x][y] = frames * delay;
2986 if (MovDelay[x][y]) // wait some time before next frame
2989 int wall_mask = Store2[x][y];
2990 int real_element = Tile[x][y] - EL_WALL_CHANGING_BASE + EL_WALL_AMOEBA_BASE;
2993 phase = MovDelay[x][y] / delay;
2995 if (!MovDelay[x][y])
2997 Tile[x][y] = real_element;
2998 Store[x][y] = Store2[x][y] = 0;
3000 DrawWalls_MM(x, y, Tile[x][y]);
3001 DrawLaser(0, DL_LASER_ENABLED);
3003 else if (!(MovDelay[x][y] % delay) && IN_SCR_FIELD(x, y))
3005 DrawWallsAnimation_MM(x, y, real_element, phase, wall_mask);
3010 static void DrawFieldAnimated_MM(int x, int y)
3014 laser.redraw = TRUE;
3017 static void DrawFieldAnimatedIfNeeded_MM(int x, int y)
3019 int element = Tile[x][y];
3020 int graphic = el2gfx(element);
3022 if (!getGraphicInfo_NewFrame(x, y, graphic))
3027 laser.redraw = TRUE;
3030 static void DrawFieldTwinkle(int x, int y)
3032 if (MovDelay[x][y] != 0) // wait some time before next frame
3038 if (MovDelay[x][y] != 0)
3040 int graphic = IMG_TWINKLE_WHITE;
3041 int frame = getGraphicAnimationFrame(graphic, 10 - MovDelay[x][y]);
3043 DrawGraphicThruMask_MM(SCREENX(x), SCREENY(y), graphic, frame);
3046 laser.redraw = TRUE;
3050 static void Explode_MM(int x, int y, int phase, int mode)
3052 int num_phase = 9, delay = 2;
3053 int last_phase = num_phase * delay;
3054 int half_phase = (num_phase / 2) * delay;
3057 laser.redraw = TRUE;
3059 if (phase == EX_PHASE_START) // initialize 'Store[][]' field
3061 center_element = Tile[x][y];
3063 if (IS_MOVING(x, y) || IS_BLOCKED(x, y))
3065 // put moving element to center field (and let it explode there)
3066 center_element = MovingOrBlocked2Element_MM(x, y);
3067 RemoveMovingField_MM(x, y);
3069 Tile[x][y] = center_element;
3072 if (center_element != EL_GRAY_BALL_ACTIVE)
3073 Store[x][y] = EL_EMPTY;
3074 Store2[x][y] = center_element;
3076 Tile[x][y] = EL_EXPLODING_OPAQUE;
3078 GfxElement[x][y] = (center_element == EL_BOMB_ACTIVE ? EL_BOMB :
3079 center_element == EL_GRAY_BALL_ACTIVE ? EL_GRAY_BALL :
3080 IS_MCDUFFIN(center_element) ? EL_MCDUFFIN :
3083 MovDir[x][y] = MovPos[x][y] = MovDelay[x][y] = 0;
3085 ExplodePhase[x][y] = 1;
3091 GfxFrame[x][y] = 0; // restart explosion animation
3093 ExplodePhase[x][y] = (phase < last_phase ? phase + 1 : 0);
3095 center_element = Store2[x][y];
3097 if (phase == half_phase && Store[x][y] == EL_EMPTY)
3099 Tile[x][y] = EL_EXPLODING_TRANSP;
3101 if (x == ELX && y == ELY)
3105 if (phase == last_phase)
3107 if (center_element == EL_BOMB_ACTIVE)
3109 DrawLaser(0, DL_LASER_DISABLED);
3112 Bang_MM(laser.start_edge.x, laser.start_edge.y);
3114 laser.overloaded = FALSE;
3116 else if (IS_MCDUFFIN(center_element) || IS_LASER(center_element))
3118 GameOver_MM(GAME_OVER_BOMB);
3121 Tile[x][y] = Store[x][y];
3123 Store[x][y] = Store2[x][y] = 0;
3124 MovDir[x][y] = MovPos[x][y] = MovDelay[x][y] = 0;
3126 InitField(x, y, FALSE);
3129 if (center_element == EL_GRAY_BALL_ACTIVE)
3130 ScanLaser_FromLastMirror();
3132 else if (!(phase % delay) && IN_SCR_FIELD(SCREENX(x), SCREENY(y)))
3134 int graphic = el_act2gfx(GfxElement[x][y], MM_ACTION_EXPLODING);
3135 int frame = getGraphicAnimationFrameXY(graphic, x, y);
3137 DrawGraphicAnimation_MM(x, y, graphic, frame);
3139 MarkTileDirty(x, y);
3143 static void Bang_MM(int x, int y)
3145 int element = Tile[x][y];
3147 if (IS_PACMAN(element))
3148 PlayLevelSound_MM(x, y, element, MM_ACTION_EXPLODING);
3149 else if (element == EL_BOMB_ACTIVE || IS_MCDUFFIN(element))
3150 PlayLevelSound_MM(x, y, element, MM_ACTION_EXPLODING);
3151 else if (element == EL_KEY)
3152 PlayLevelSound_MM(x, y, element, MM_ACTION_EXPLODING);
3154 PlayLevelSound_MM(x, y, element, MM_ACTION_EXPLODING);
3156 Explode_MM(x, y, EX_PHASE_START, EX_TYPE_NORMAL);
3159 static void TurnRound(int x, int y)
3171 { 0, 0 }, { 0, 0 }, { 0, 0 },
3176 int left, right, back;
3180 { MV_DOWN, MV_UP, MV_RIGHT },
3181 { MV_UP, MV_DOWN, MV_LEFT },
3183 { MV_LEFT, MV_RIGHT, MV_DOWN },
3187 { MV_RIGHT, MV_LEFT, MV_UP }
3190 int element = Tile[x][y];
3191 int old_move_dir = MovDir[x][y];
3192 int right_dir = turn[old_move_dir].right;
3193 int back_dir = turn[old_move_dir].back;
3194 int right_dx = move_xy[right_dir].x, right_dy = move_xy[right_dir].y;
3195 int right_x = x + right_dx, right_y = y + right_dy;
3197 if (element == EL_PACMAN)
3199 boolean can_turn_right = FALSE;
3201 if (IN_LEV_FIELD(right_x, right_y) &&
3202 IS_EATABLE4PACMAN(Tile[right_x][right_y]))
3203 can_turn_right = TRUE;
3206 MovDir[x][y] = right_dir;
3208 MovDir[x][y] = back_dir;
3214 static void StartMoving_MM(int x, int y)
3216 int element = Tile[x][y];
3221 if (CAN_MOVE(element))
3225 if (MovDelay[x][y]) // wait some time before next movement
3233 // now make next step
3235 Moving2Blocked(x, y, &newx, &newy); // get next screen position
3237 if (element == EL_PACMAN &&
3238 IN_LEV_FIELD(newx, newy) && IS_EATABLE4PACMAN(Tile[newx][newy]) &&
3239 !ObjHit(newx, newy, HIT_POS_CENTER))
3241 Store[newx][newy] = Tile[newx][newy];
3242 Tile[newx][newy] = EL_EMPTY;
3244 DrawField_MM(newx, newy);
3246 else if (!IN_LEV_FIELD(newx, newy) || !IS_FREE(newx, newy) ||
3247 ObjHit(newx, newy, HIT_POS_CENTER))
3249 // object was running against a wall
3256 InitMovingField_MM(x, y, MovDir[x][y]);
3260 ContinueMoving_MM(x, y);
3263 static void ContinueMoving_MM(int x, int y)
3265 int element = Tile[x][y];
3266 int direction = MovDir[x][y];
3267 int dx = (direction == MV_LEFT ? -1 : direction == MV_RIGHT ? +1 : 0);
3268 int dy = (direction == MV_UP ? -1 : direction == MV_DOWN ? +1 : 0);
3269 int horiz_move = (dx!=0);
3270 int newx = x + dx, newy = y + dy;
3271 int step = (horiz_move ? dx : dy) * TILEX / 8;
3273 MovPos[x][y] += step;
3275 if (ABS(MovPos[x][y]) >= TILEX) // object reached its destination
3277 Tile[x][y] = EL_EMPTY;
3278 Tile[newx][newy] = element;
3280 MovPos[x][y] = MovDir[x][y] = MovDelay[x][y] = 0;
3281 MovDelay[newx][newy] = 0;
3283 if (!CAN_MOVE(element))
3284 MovDir[newx][newy] = 0;
3287 DrawField_MM(newx, newy);
3289 Stop[newx][newy] = TRUE;
3291 if (element == EL_PACMAN)
3293 if (Store[newx][newy] == EL_BOMB)
3294 Bang_MM(newx, newy);
3296 if (IS_WALL_AMOEBA(Store[newx][newy]) &&
3297 (LX + 2 * XS) / TILEX == newx &&
3298 (LY + 2 * YS) / TILEY == newy)
3305 else // still moving on
3310 laser.redraw = TRUE;
3313 boolean ClickElement(int x, int y, int button)
3315 static DelayCounter click_delay = { CLICK_DELAY };
3316 static boolean new_button = TRUE;
3317 boolean element_clicked = FALSE;
3322 // initialize static variables
3323 click_delay.count = 0;
3324 click_delay.value = CLICK_DELAY;
3330 // do not rotate objects hit by the laser after the game was solved
3331 if (game_mm.level_solved && Hit[x][y])
3334 if (button == MB_RELEASED)
3337 click_delay.value = CLICK_DELAY;
3339 // release eventually hold auto-rotating mirror
3340 RotateMirror(x, y, MB_RELEASED);
3345 if (!FrameReached(&click_delay) && !new_button)
3348 if (button == MB_MIDDLEBUTTON) // middle button has no function
3351 if (!IN_LEV_FIELD(x, y))
3354 if (Tile[x][y] == EL_EMPTY)
3357 element = Tile[x][y];
3359 if (IS_MIRROR(element) ||
3360 IS_BEAMER(element) ||
3361 IS_POLAR(element) ||
3362 IS_POLAR_CROSS(element) ||
3363 IS_DF_MIRROR(element) ||
3364 IS_DF_MIRROR_AUTO(element))
3366 RotateMirror(x, y, button);
3368 element_clicked = TRUE;
3370 else if (IS_MCDUFFIN(element))
3372 boolean has_laser = (x == laser.start_edge.x && y == laser.start_edge.y);
3374 if (has_laser && !laser.fuse_off)
3375 DrawLaser(0, DL_LASER_DISABLED);
3377 element = get_rotated_element(element, BUTTON_ROTATION(button));
3379 Tile[x][y] = element;
3384 laser.start_angle = get_element_angle(element);
3388 if (!laser.fuse_off)
3392 element_clicked = TRUE;
3394 else if (element == EL_FUSE_ON && laser.fuse_off)
3396 if (x != laser.fuse_x || y != laser.fuse_y)
3399 laser.fuse_off = FALSE;
3400 laser.fuse_x = laser.fuse_y = -1;
3402 DrawGraphic_MM(x, y, IMG_MM_FUSE_ACTIVE);
3405 element_clicked = TRUE;
3407 else if (element == EL_FUSE_ON && !laser.fuse_off && new_button)
3409 laser.fuse_off = TRUE;
3412 laser.overloaded = FALSE;
3414 DrawLaser(0, DL_LASER_DISABLED);
3415 DrawGraphic_MM(x, y, IMG_MM_FUSE);
3417 element_clicked = TRUE;
3419 else if (element == EL_LIGHTBALL)
3422 RaiseScoreElement_MM(element);
3423 DrawLaser(0, DL_LASER_ENABLED);
3425 element_clicked = TRUE;
3427 else if (IS_ENVELOPE(element))
3429 Tile[x][y] = EL_ENVELOPE_1_OPENING + ENVELOPE_NR(element);
3431 element_clicked = TRUE;
3434 click_delay.value = (new_button ? CLICK_DELAY_FIRST : CLICK_DELAY);
3437 return element_clicked;
3440 static void RotateMirror(int x, int y, int button)
3442 if (button == MB_RELEASED)
3444 // release eventually hold auto-rotating mirror
3451 if (IS_MIRROR(Tile[x][y]) ||
3452 IS_POLAR_CROSS(Tile[x][y]) ||
3453 IS_POLAR(Tile[x][y]) ||
3454 IS_BEAMER(Tile[x][y]) ||
3455 IS_DF_MIRROR(Tile[x][y]) ||
3456 IS_GRID_STEEL_AUTO(Tile[x][y]) ||
3457 IS_GRID_WOOD_AUTO(Tile[x][y]))
3459 Tile[x][y] = get_rotated_element(Tile[x][y], BUTTON_ROTATION(button));
3461 else if (IS_DF_MIRROR_AUTO(Tile[x][y]))
3463 if (button == MB_LEFTBUTTON)
3465 // left mouse button only for manual adjustment, no auto-rotating;
3466 // freeze mirror for until mouse button released
3470 else if (button == MB_RIGHTBUTTON && (hold_x != x || hold_y != y))
3472 Tile[x][y] = get_rotated_element(Tile[x][y], ROTATE_RIGHT);
3476 if (IS_GRID_STEEL_AUTO(Tile[x][y]) || IS_GRID_WOOD_AUTO(Tile[x][y]))
3478 int edge = Hit[x][y];
3484 DrawLaser(edge - 1, DL_LASER_DISABLED);
3488 else if (ObjHit(x, y, HIT_POS_CENTER))
3490 int edge = Hit[x][y];
3494 Warn("RotateMirror: inconsistent field Hit[][]!\n");
3499 DrawLaser(edge - 1, DL_LASER_DISABLED);
3506 if (ObjHit(x, y, HIT_POS_EDGE | HIT_POS_BETWEEN))
3511 if ((IS_BEAMER(Tile[x][y]) ||
3512 IS_POLAR(Tile[x][y]) ||
3513 IS_POLAR_CROSS(Tile[x][y])) && x == ELX && y == ELY)
3515 if (IS_BEAMER(Tile[x][y]))
3518 Debug("game:mm:RotateMirror", "TEST (%d, %d) [%d] [%d]",
3519 LX, LY, laser.beamer_edge, laser.beamer[1].num);
3532 DrawLaser(0, DL_LASER_ENABLED);
3536 static void AutoRotateMirrors(void)
3540 if (!FrameReached(&rotate_delay))
3543 for (x = 0; x < lev_fieldx; x++)
3545 for (y = 0; y < lev_fieldy; y++)
3547 int element = Tile[x][y];
3549 // do not rotate objects hit by the laser after the game was solved
3550 if (game_mm.level_solved && Hit[x][y])
3553 if (IS_DF_MIRROR_AUTO(element) ||
3554 IS_GRID_WOOD_AUTO(element) ||
3555 IS_GRID_STEEL_AUTO(element) ||
3556 element == EL_REFRACTOR)
3558 RotateMirror(x, y, MB_RIGHTBUTTON);
3560 laser.redraw = TRUE;
3566 static boolean ObjHit(int obx, int oby, int bits)
3573 if (bits & HIT_POS_CENTER)
3575 if (CheckLaserPixel(cSX + obx + 15,
3580 if (bits & HIT_POS_EDGE)
3582 for (i = 0; i < 4; i++)
3583 if (CheckLaserPixel(cSX + obx + 31 * (i % 2),
3584 cSY + oby + 31 * (i / 2)))
3588 if (bits & HIT_POS_BETWEEN)
3590 for (i = 0; i < 4; i++)
3591 if (CheckLaserPixel(cSX + 4 + obx + 22 * (i % 2),
3592 cSY + 4 + oby + 22 * (i / 2)))
3599 static void DeletePacMan(int px, int py)
3605 if (game_mm.num_pacman <= 1)
3607 game_mm.num_pacman = 0;
3611 for (i = 0; i < game_mm.num_pacman; i++)
3612 if (game_mm.pacman[i].x == px && game_mm.pacman[i].y == py)
3615 game_mm.num_pacman--;
3617 for (j = i; j < game_mm.num_pacman; j++)
3619 game_mm.pacman[j].x = game_mm.pacman[j + 1].x;
3620 game_mm.pacman[j].y = game_mm.pacman[j + 1].y;
3621 game_mm.pacman[j].dir = game_mm.pacman[j + 1].dir;
3625 static void GameActions_MM_Ext(void)
3632 for (y = 0; y < lev_fieldy; y++) for (x = 0; x < lev_fieldx; x++)
3635 for (y = 0; y < lev_fieldy; y++) for (x = 0; x < lev_fieldx; x++)
3637 element = Tile[x][y];
3639 if (!IS_MOVING(x, y) && CAN_MOVE(element))
3640 StartMoving_MM(x, y);
3641 else if (IS_MOVING(x, y))
3642 ContinueMoving_MM(x, y);
3643 else if (IS_EXPLODING(element))
3644 Explode_MM(x, y, ExplodePhase[x][y], EX_TYPE_NORMAL);
3645 else if (element == EL_EXIT_OPENING)
3647 else if (element == EL_GRAY_BALL_OPENING)
3649 else if (IS_ENVELOPE_OPENING(element))
3651 else if (IS_WALL_CHANGING(element) && Store[x][y] == EL_WALL_ICE_BASE)
3653 else if (IS_WALL_CHANGING(element) && Store[x][y] == EL_WALL_AMOEBA_BASE)
3655 else if (IS_MIRROR(element) ||
3656 IS_MIRROR_FIXED(element) ||
3657 element == EL_PRISM)
3658 DrawFieldTwinkle(x, y);
3659 else if (element == EL_GRAY_BALL_ACTIVE ||
3660 element == EL_BOMB_ACTIVE ||
3661 element == EL_MINE_ACTIVE)
3662 DrawFieldAnimated_MM(x, y);
3663 else if (!IS_BLOCKED(x, y))
3664 DrawFieldAnimatedIfNeeded_MM(x, y);
3667 AutoRotateMirrors();
3670 // !!! CHANGE THIS: REDRAW ONLY WHEN NEEDED !!!
3672 // redraw after Explode_MM() ...
3674 DrawLaser(0, DL_LASER_ENABLED);
3675 laser.redraw = FALSE;
3680 if (game_mm.num_pacman && FrameReached(&pacman_delay))
3684 if (laser.num_damages > MAX_LASER_LEN && !laser.fuse_off)
3686 DrawLaser(0, DL_LASER_DISABLED);
3691 // skip all following game actions if game is over
3692 if (game_mm.game_over)
3695 if (game_mm.energy_left == 0 && !game.no_level_time_limit && game.time_limit)
3699 GameOver_MM(GAME_OVER_NO_ENERGY);
3704 if (FrameReached(&energy_delay))
3706 if (game_mm.energy_left > 0)
3707 game_mm.energy_left--;
3709 // when out of energy, wait another frame to play "out of time" sound
3712 element = laser.dest_element;
3715 if (element != Tile[ELX][ELY])
3717 Debug("game:mm:GameActions_MM_Ext", "element == %d, Tile[ELX][ELY] == %d",
3718 element, Tile[ELX][ELY]);
3722 if (!laser.overloaded && laser.overload_value == 0 &&
3723 element != EL_BOMB &&
3724 element != EL_BOMB_ACTIVE &&
3725 element != EL_MINE &&
3726 element != EL_MINE_ACTIVE &&
3727 element != EL_GRAY_BALL &&
3728 element != EL_GRAY_BALL_ACTIVE &&
3729 element != EL_BLOCK_STONE &&
3730 element != EL_BLOCK_WOOD &&
3731 element != EL_FUSE_ON &&
3732 element != EL_FUEL_FULL &&
3733 !IS_WALL_ICE(element) &&
3734 !IS_WALL_AMOEBA(element))
3737 overload_delay.value = HEALTH_DELAY(laser.overloaded);
3739 if (((laser.overloaded && laser.overload_value < MAX_LASER_OVERLOAD) ||
3740 (!laser.overloaded && laser.overload_value > 0)) &&
3741 FrameReached(&overload_delay))
3743 if (laser.overloaded)
3744 laser.overload_value++;
3746 laser.overload_value--;
3748 if (game_mm.cheat_no_overload)
3750 laser.overloaded = FALSE;
3751 laser.overload_value = 0;
3754 game_mm.laser_overload_value = laser.overload_value;
3756 if (laser.overload_value < MAX_LASER_OVERLOAD - 8)
3758 SetLaserColor(0xFF);
3760 DrawLaser(0, DL_LASER_ENABLED);
3763 if (!laser.overloaded)
3764 StopSound_MM(SND_MM_GAME_HEALTH_CHARGING);
3765 else if (setup.sound_loops)
3766 PlaySoundLoop_MM(SND_MM_GAME_HEALTH_CHARGING);
3768 PlaySound_MM(SND_MM_GAME_HEALTH_CHARGING);
3770 if (laser.overload_value == MAX_LASER_OVERLOAD)
3772 UpdateAndDisplayGameControlValues();
3776 GameOver_MM(GAME_OVER_OVERLOADED);
3787 if (element == EL_BOMB && CT > native_mm_level.time_bomb)
3789 if (game_mm.cheat_no_explosion)
3794 laser.dest_element = EL_EXPLODING_OPAQUE;
3799 if (element == EL_FUSE_ON && CT > native_mm_level.time_fuse)
3801 laser.fuse_off = TRUE;
3805 DrawLaser(0, DL_LASER_DISABLED);
3806 DrawGraphic_MM(ELX, ELY, IMG_MM_FUSE);
3809 if (element == EL_GRAY_BALL && CT > native_mm_level.time_ball)
3811 if (!Store2[ELX][ELY]) // check if content element not yet determined
3813 int last_anim_random_frame = gfx.anim_random_frame;
3816 if (native_mm_level.ball_choice_mode == ANIM_RANDOM)
3817 gfx.anim_random_frame = RND(native_mm_level.num_ball_contents);
3819 element_pos = getAnimationFrame(native_mm_level.num_ball_contents, 1,
3820 native_mm_level.ball_choice_mode, 0,
3821 game_mm.ball_choice_pos);
3823 if (native_mm_level.ball_choice_mode == ANIM_RANDOM)
3824 gfx.anim_random_frame = last_anim_random_frame;
3826 game_mm.ball_choice_pos++;
3828 int new_element = native_mm_level.ball_content[element_pos];
3829 int new_element_base = map_wall_to_base_element(new_element);
3831 if (IS_WALL(new_element_base))
3833 // always use completely filled wall element
3834 new_element = new_element_base | 0x000f;
3836 else if (native_mm_level.rotate_ball_content &&
3837 get_num_elements(new_element) > 1)
3839 // randomly rotate newly created game element
3840 new_element = get_rotated_element(new_element, RND(16));
3843 Store[ELX][ELY] = new_element;
3844 Store2[ELX][ELY] = TRUE;
3847 if (native_mm_level.explode_ball)
3850 Tile[ELX][ELY] = EL_GRAY_BALL_OPENING;
3852 laser.dest_element = laser.dest_element_last = Tile[ELX][ELY];
3857 if (IS_WALL_ICE(element) && CT > 50)
3859 PlayLevelSound_MM(ELX, ELY, element, MM_ACTION_SHRINKING);
3861 Tile[ELX][ELY] = Tile[ELX][ELY] - EL_WALL_ICE_BASE + EL_WALL_CHANGING_BASE;
3862 Store[ELX][ELY] = EL_WALL_ICE_BASE;
3863 Store2[ELX][ELY] = laser.wall_mask;
3865 laser.dest_element = Tile[ELX][ELY];
3870 if (IS_WALL_AMOEBA(element) && CT > 60)
3873 int element2 = Tile[ELX][ELY];
3875 if (element2 != EL_EMPTY && !IS_WALL_AMOEBA(element))
3878 for (i = laser.num_damages - 1; i >= 0; i--)
3879 if (laser.damage[i].is_mirror)
3882 r = laser.num_edges;
3883 d = laser.num_damages;
3890 DrawLaser(laser.damage[k1].edge - 1, DL_LASER_DISABLED);
3893 DrawLaser(0, DL_LASER_ENABLED);
3896 x = laser.damage[k1].x;
3897 y = laser.damage[k1].y;
3902 for (i = 0; i < 4; i++)
3904 if (laser.wall_mask & (1 << i))
3906 if (CheckLaserPixel(cSX + ELX * TILEX + 14 + (i % 2) * 2,
3907 cSY + ELY * TILEY + 31 * (i / 2)))
3910 if (CheckLaserPixel(cSX + ELX * TILEX + 31 * (i % 2),
3911 cSY + ELY * TILEY + 14 + (i / 2) * 2))
3918 for (i = 0; i < 4; i++)
3920 if (laser.wall_mask & (1 << i))
3922 if (CheckLaserPixel(cSX + ELX * TILEX + 31 * (i % 2),
3923 cSY + ELY * TILEY + 31 * (i / 2)))
3930 if (laser.num_beamers > 0 ||
3931 k1 < 1 || k2 < 4 || k3 < 4 ||
3932 CheckLaserPixel(cSX + ELX * TILEX + 14,
3933 cSY + ELY * TILEY + 14))
3935 laser.num_edges = r;
3936 laser.num_damages = d;
3938 DrawLaser(0, DL_LASER_DISABLED);
3941 Tile[ELX][ELY] = element | laser.wall_mask;
3943 int x = ELX, y = ELY;
3944 int wall_mask = laser.wall_mask;
3947 DrawLaser(0, DL_LASER_ENABLED);
3949 PlayLevelSound_MM(x, y, element, MM_ACTION_GROWING);
3951 Tile[x][y] = Tile[x][y] - EL_WALL_AMOEBA_BASE + EL_WALL_CHANGING_BASE;
3952 Store[x][y] = EL_WALL_AMOEBA_BASE;
3953 Store2[x][y] = wall_mask;
3958 if ((element == EL_BLOCK_WOOD || element == EL_BLOCK_STONE) &&
3959 laser.stops_inside_element && CT > native_mm_level.time_block)
3964 if (ABS(XS) > ABS(YS))
3971 for (i = 0; i < 4; i++)
3978 x = ELX + Step[k * 4].x;
3979 y = ELY + Step[k * 4].y;
3981 if (!IN_LEV_FIELD(x, y) || Tile[x][y] != EL_EMPTY)
3984 if (ObjHit(x, y, HIT_POS_CENTER | HIT_POS_EDGE | HIT_POS_BETWEEN))
3992 laser.overloaded = (element == EL_BLOCK_STONE);
3997 PlayLevelSound_MM(ELX, ELY, element, MM_ACTION_PUSHING);
4000 Tile[x][y] = element;
4002 DrawGraphic_MM(ELX, ELY, IMG_EMPTY);
4005 if (element == EL_BLOCK_STONE && Box[ELX][ELY])
4007 DrawLaser(Box[ELX][ELY] - 1, DL_LASER_DISABLED);
4008 DrawLaser(laser.num_edges - 1, DL_LASER_ENABLED);
4016 if (element == EL_FUEL_FULL && CT > 10)
4018 int num_init_game_frames = INIT_GAME_ACTIONS_DELAY;
4019 int start = num_init_game_frames * game_mm.energy_left / native_mm_level.time;
4021 for (i = start; i <= num_init_game_frames; i++)
4023 if (i == num_init_game_frames)
4024 StopSound_MM(SND_MM_GAME_LEVELTIME_CHARGING);
4025 else if (setup.sound_loops)
4026 PlaySoundLoop_MM(SND_MM_GAME_LEVELTIME_CHARGING);
4028 PlaySound_MM(SND_MM_GAME_LEVELTIME_CHARGING);
4030 game_mm.energy_left = native_mm_level.time * i / num_init_game_frames;
4032 UpdateAndDisplayGameControlValues();
4037 Tile[ELX][ELY] = laser.dest_element = EL_FUEL_EMPTY;
4039 DrawField_MM(ELX, ELY);
4041 DrawLaser(0, DL_LASER_ENABLED);
4047 void GameActions_MM(struct MouseActionInfo action)
4049 boolean element_clicked = ClickElement(action.lx, action.ly, action.button);
4050 boolean button_released = (action.button == MB_RELEASED);
4052 GameActions_MM_Ext();
4054 CheckSingleStepMode_MM(element_clicked, button_released);
4057 static void MovePacMen(void)
4059 int mx, my, ox, oy, nx, ny;
4063 if (++pacman_nr >= game_mm.num_pacman)
4066 game_mm.pacman[pacman_nr].dir--;
4068 for (l = 1; l < 5; l++)
4070 game_mm.pacman[pacman_nr].dir++;
4072 if (game_mm.pacman[pacman_nr].dir > 4)
4073 game_mm.pacman[pacman_nr].dir = 1;
4075 if (game_mm.pacman[pacman_nr].dir % 2)
4078 my = game_mm.pacman[pacman_nr].dir - 2;
4083 mx = 3 - game_mm.pacman[pacman_nr].dir;
4086 ox = game_mm.pacman[pacman_nr].x;
4087 oy = game_mm.pacman[pacman_nr].y;
4090 element = Tile[nx][ny];
4092 if (nx < 0 || nx > 15 || ny < 0 || ny > 11)
4095 if (!IS_EATABLE4PACMAN(element))
4098 if (ObjHit(nx, ny, HIT_POS_CENTER))
4101 Tile[ox][oy] = EL_EMPTY;
4103 EL_PACMAN_RIGHT - 1 +
4104 (game_mm.pacman[pacman_nr].dir - 1 +
4105 (game_mm.pacman[pacman_nr].dir % 2) * 2);
4107 game_mm.pacman[pacman_nr].x = nx;
4108 game_mm.pacman[pacman_nr].y = ny;
4110 DrawGraphic_MM(ox, oy, IMG_EMPTY);
4112 if (element != EL_EMPTY)
4114 int graphic = el2gfx(Tile[nx][ny]);
4119 getGraphicSource(graphic, 0, &bitmap, &src_x, &src_y);
4122 ox = cSX + ox * TILEX;
4123 oy = cSY + oy * TILEY;
4125 for (i = 1; i < 33; i += 2)
4126 BlitBitmap(bitmap, window,
4127 src_x, src_y, TILEX, TILEY,
4128 ox + i * mx, oy + i * my);
4129 Ct = Ct + FrameCounter - CT;
4132 DrawField_MM(nx, ny);
4135 if (!laser.fuse_off)
4137 DrawLaser(0, DL_LASER_ENABLED);
4139 if (ObjHit(nx, ny, HIT_POS_BETWEEN))
4141 AddDamagedField(nx, ny);
4143 laser.damage[laser.num_damages - 1].edge = 0;
4147 if (element == EL_BOMB)
4148 DeletePacMan(nx, ny);
4150 if (IS_WALL_AMOEBA(element) &&
4151 (LX + 2 * XS) / TILEX == nx &&
4152 (LY + 2 * YS) / TILEY == ny)
4162 static void InitMovingField_MM(int x, int y, int direction)
4164 int newx = x + (direction == MV_LEFT ? -1 : direction == MV_RIGHT ? +1 : 0);
4165 int newy = y + (direction == MV_UP ? -1 : direction == MV_DOWN ? +1 : 0);
4167 MovDir[x][y] = direction;
4168 MovDir[newx][newy] = direction;
4170 if (Tile[newx][newy] == EL_EMPTY)
4171 Tile[newx][newy] = EL_BLOCKED;
4174 static int MovingOrBlocked2Element_MM(int x, int y)
4176 int element = Tile[x][y];
4178 if (element == EL_BLOCKED)
4182 Blocked2Moving(x, y, &oldx, &oldy);
4184 return Tile[oldx][oldy];
4190 static void RemoveMovingField_MM(int x, int y)
4192 int oldx = x, oldy = y, newx = x, newy = y;
4194 if (Tile[x][y] != EL_BLOCKED && !IS_MOVING(x, y))
4197 if (IS_MOVING(x, y))
4199 Moving2Blocked(x, y, &newx, &newy);
4200 if (Tile[newx][newy] != EL_BLOCKED)
4203 else if (Tile[x][y] == EL_BLOCKED)
4205 Blocked2Moving(x, y, &oldx, &oldy);
4206 if (!IS_MOVING(oldx, oldy))
4210 Tile[oldx][oldy] = EL_EMPTY;
4211 Tile[newx][newy] = EL_EMPTY;
4212 MovPos[oldx][oldy] = MovDir[oldx][oldy] = MovDelay[oldx][oldy] = 0;
4213 MovPos[newx][newy] = MovDir[newx][newy] = MovDelay[newx][newy] = 0;
4215 DrawLevelField_MM(oldx, oldy);
4216 DrawLevelField_MM(newx, newy);
4219 static void RaiseScore_MM(int value)
4221 game_mm.score += value;
4224 void RaiseScoreElement_MM(int element)
4229 case EL_PACMAN_RIGHT:
4231 case EL_PACMAN_LEFT:
4232 case EL_PACMAN_DOWN:
4233 RaiseScore_MM(native_mm_level.score[SC_PACMAN]);
4237 RaiseScore_MM(native_mm_level.score[SC_KEY]);
4242 RaiseScore_MM(native_mm_level.score[SC_COLLECTIBLE]);
4246 RaiseScore_MM(native_mm_level.score[SC_LIGHTBALL]);
4255 // ----------------------------------------------------------------------------
4256 // Mirror Magic game engine snapshot handling functions
4257 // ----------------------------------------------------------------------------
4259 void SaveEngineSnapshotValues_MM(void)
4263 engine_snapshot_mm.game_mm = game_mm;
4264 engine_snapshot_mm.laser = laser;
4266 for (x = 0; x < MAX_PLAYFIELD_WIDTH; x++)
4268 for (y = 0; y < MAX_PLAYFIELD_HEIGHT; y++)
4270 engine_snapshot_mm.Ur[x][y] = Ur[x][y];
4271 engine_snapshot_mm.Hit[x][y] = Hit[x][y];
4272 engine_snapshot_mm.Box[x][y] = Box[x][y];
4273 engine_snapshot_mm.Angle[x][y] = Angle[x][y];
4277 engine_snapshot_mm.LX = LX;
4278 engine_snapshot_mm.LY = LY;
4279 engine_snapshot_mm.XS = XS;
4280 engine_snapshot_mm.YS = YS;
4281 engine_snapshot_mm.ELX = ELX;
4282 engine_snapshot_mm.ELY = ELY;
4283 engine_snapshot_mm.CT = CT;
4284 engine_snapshot_mm.Ct = Ct;
4286 engine_snapshot_mm.last_LX = last_LX;
4287 engine_snapshot_mm.last_LY = last_LY;
4288 engine_snapshot_mm.last_hit_mask = last_hit_mask;
4289 engine_snapshot_mm.hold_x = hold_x;
4290 engine_snapshot_mm.hold_y = hold_y;
4291 engine_snapshot_mm.pacman_nr = pacman_nr;
4293 engine_snapshot_mm.rotate_delay = rotate_delay;
4294 engine_snapshot_mm.pacman_delay = pacman_delay;
4295 engine_snapshot_mm.energy_delay = energy_delay;
4296 engine_snapshot_mm.overload_delay = overload_delay;
4299 void LoadEngineSnapshotValues_MM(void)
4303 // stored engine snapshot buffers already restored at this point
4305 game_mm = engine_snapshot_mm.game_mm;
4306 laser = engine_snapshot_mm.laser;
4308 for (x = 0; x < MAX_PLAYFIELD_WIDTH; x++)
4310 for (y = 0; y < MAX_PLAYFIELD_HEIGHT; y++)
4312 Ur[x][y] = engine_snapshot_mm.Ur[x][y];
4313 Hit[x][y] = engine_snapshot_mm.Hit[x][y];
4314 Box[x][y] = engine_snapshot_mm.Box[x][y];
4315 Angle[x][y] = engine_snapshot_mm.Angle[x][y];
4319 LX = engine_snapshot_mm.LX;
4320 LY = engine_snapshot_mm.LY;
4321 XS = engine_snapshot_mm.XS;
4322 YS = engine_snapshot_mm.YS;
4323 ELX = engine_snapshot_mm.ELX;
4324 ELY = engine_snapshot_mm.ELY;
4325 CT = engine_snapshot_mm.CT;
4326 Ct = engine_snapshot_mm.Ct;
4328 last_LX = engine_snapshot_mm.last_LX;
4329 last_LY = engine_snapshot_mm.last_LY;
4330 last_hit_mask = engine_snapshot_mm.last_hit_mask;
4331 hold_x = engine_snapshot_mm.hold_x;
4332 hold_y = engine_snapshot_mm.hold_y;
4333 pacman_nr = engine_snapshot_mm.pacman_nr;
4335 rotate_delay = engine_snapshot_mm.rotate_delay;
4336 pacman_delay = engine_snapshot_mm.pacman_delay;
4337 energy_delay = engine_snapshot_mm.energy_delay;
4338 overload_delay = engine_snapshot_mm.overload_delay;
4340 RedrawPlayfield_MM();
4343 static int getAngleFromTouchDelta(int dx, int dy, int base)
4345 double pi = 3.141592653;
4346 double rad = atan2((double)-dy, (double)dx);
4347 double rad2 = (rad < 0 ? rad + 2 * pi : rad);
4348 double deg = rad2 * 180.0 / pi;
4350 return (int)(deg * base / 360.0 + 0.5) % base;
4353 int getButtonFromTouchPosition(int x, int y, int dst_mx, int dst_my)
4355 // calculate start (source) position to be at the middle of the tile
4356 int src_mx = cSX + x * TILESIZE_VAR + TILESIZE_VAR / 2;
4357 int src_my = cSY + y * TILESIZE_VAR + TILESIZE_VAR / 2;
4358 int dx = dst_mx - src_mx;
4359 int dy = dst_my - src_my;
4368 if (!IN_LEV_FIELD(x, y))
4371 element = Tile[x][y];
4373 if (!IS_MCDUFFIN(element) &&
4374 !IS_MIRROR(element) &&
4375 !IS_BEAMER(element) &&
4376 !IS_POLAR(element) &&
4377 !IS_POLAR_CROSS(element) &&
4378 !IS_DF_MIRROR(element))
4381 angle_old = get_element_angle(element);
4383 if (IS_MCDUFFIN(element))
4385 angle_new = (dx > 0 && ABS(dy) < ABS(dx) ? ANG_RAY_RIGHT :
4386 dy < 0 && ABS(dx) < ABS(dy) ? ANG_RAY_UP :
4387 dx < 0 && ABS(dy) < ABS(dx) ? ANG_RAY_LEFT :
4388 dy > 0 && ABS(dx) < ABS(dy) ? ANG_RAY_DOWN :
4391 else if (IS_MIRROR(element) ||
4392 IS_DF_MIRROR(element))
4394 for (i = 0; i < laser.num_damages; i++)
4396 if (laser.damage[i].x == x &&
4397 laser.damage[i].y == y &&
4398 ObjHit(x, y, HIT_POS_CENTER))
4400 angle_old = get_mirrored_angle(laser.damage[i].angle, angle_old);
4401 angle_new = getAngleFromTouchDelta(dx, dy, base) % phases;
4408 if (angle_new == -1)
4410 if (IS_MIRROR(element) ||
4411 IS_DF_MIRROR(element) ||
4415 if (IS_POLAR_CROSS(element))
4418 angle_new = getAngleFromTouchDelta(dx, dy, base) % phases;
4421 button = (angle_new == angle_old ? 0 :
4422 (angle_new - angle_old + phases) % phases < (phases / 2) ?
4423 MB_LEFTBUTTON : MB_RIGHTBUTTON);