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))
1331 else if (IS_DF_SLOPE(element))
1333 if (hit_mask == HIT_MASK_LEFT ||
1334 hit_mask == HIT_MASK_RIGHT ||
1335 hit_mask == HIT_MASK_TOP ||
1336 hit_mask == HIT_MASK_BOTTOM)
1338 if (HitReflectingWalls(element, hit_mask))
1343 if (HitElement(element, hit_mask))
1349 if (HitElement(element, hit_mask))
1354 DrawLaser(rf - 1, DL_LASER_ENABLED);
1355 rf = laser.num_edges;
1357 if (!IS_DF_WALL_STEEL(element))
1359 // only used for scanning DF steel walls; reset for all other elements
1367 if (laser.dest_element != Tile[ELX][ELY])
1369 Debug("game:mm:ScanLaser",
1370 "ALARM: laser.dest_element == %d, Tile[ELX][ELY] == %d",
1371 laser.dest_element, Tile[ELX][ELY]);
1375 if (!end && !laser.stops_inside_element && !StepBehind())
1378 Debug("game:mm:ScanLaser", "Go one step back");
1384 AddLaserEdge(LX, LY);
1388 DrawLaser(rf - 1, DL_LASER_ENABLED);
1390 Ct = CT = FrameCounter;
1393 if (!IN_LEV_FIELD(ELX, ELY))
1394 Debug("game:mm:ScanLaser", "WARNING! (2) %d, %d", ELX, ELY);
1398 static void ScanLaser_FromLastMirror(void)
1400 int start_pos = (laser.num_damages > 0 ? laser.num_damages - 1 : 0);
1403 for (i = start_pos; i >= 0; i--)
1404 if (laser.damage[i].is_mirror)
1407 int start_edge = (i > 0 ? laser.damage[i].edge - 1 : 0);
1409 DrawLaser(start_edge, DL_LASER_DISABLED);
1414 static void DrawLaserExt(int start_edge, int num_edges, int mode)
1420 Debug("game:mm:DrawLaserExt", "start_edge, num_edges, mode == %d, %d, %d",
1421 start_edge, num_edges, mode);
1426 Warn("DrawLaserExt: start_edge < 0");
1433 Warn("DrawLaserExt: num_edges < 0");
1439 if (mode == DL_LASER_DISABLED)
1441 Debug("game:mm:DrawLaserExt", "Delete laser from edge %d", start_edge);
1445 // now draw the laser to the backbuffer and (if enabled) to the screen
1446 DrawLaserLines(&laser.edge[start_edge], num_edges, mode);
1448 redraw_mask |= REDRAW_FIELD;
1450 if (mode == DL_LASER_ENABLED)
1453 // after the laser was deleted, the "damaged" graphics must be restored
1454 if (laser.num_damages)
1456 int damage_start = 0;
1459 // determine the starting edge, from which graphics need to be restored
1462 for (i = 0; i < laser.num_damages; i++)
1464 if (laser.damage[i].edge == start_edge + 1)
1473 // restore graphics from this starting edge to the end of damage list
1474 for (i = damage_start; i < laser.num_damages; i++)
1476 int lx = laser.damage[i].x;
1477 int ly = laser.damage[i].y;
1478 int element = Tile[lx][ly];
1480 if (Hit[lx][ly] == laser.damage[i].edge)
1481 if (!((IS_BEAMER(element) || IS_FIBRE_OPTIC(element)) &&
1484 if (Box[lx][ly] == laser.damage[i].edge)
1487 if (IS_DRAWABLE(element))
1488 DrawField_MM(lx, ly);
1491 elx = laser.damage[damage_start].x;
1492 ely = laser.damage[damage_start].y;
1493 element = Tile[elx][ely];
1496 if (IS_BEAMER(element))
1500 for (i = 0; i < laser.num_beamers; i++)
1501 Debug("game:mm:DrawLaserExt", "-> %d", laser.beamer_edge[i]);
1503 Debug("game:mm:DrawLaserExt", "IS_BEAMER: [%d]: Hit[%d][%d] == %d [%d]",
1504 mode, elx, ely, Hit[elx][ely], start_edge);
1505 Debug("game:mm:DrawLaserExt", "IS_BEAMER: %d / %d",
1506 get_element_angle(element), laser.damage[damage_start].angle);
1510 if ((IS_BEAMER(element) || IS_FIBRE_OPTIC(element)) &&
1511 laser.num_beamers > 0 &&
1512 start_edge == laser.beamer_edge[laser.num_beamers - 1])
1514 // element is outgoing beamer
1515 laser.num_damages = damage_start + 1;
1517 if (IS_BEAMER(element))
1518 laser.current_angle = get_element_angle(element);
1522 // element is incoming beamer or other element
1523 laser.num_damages = damage_start;
1524 laser.current_angle = laser.damage[laser.num_damages].angle;
1529 // no damages but McDuffin himself (who needs to be redrawn anyway)
1531 elx = laser.start_edge.x;
1532 ely = laser.start_edge.y;
1533 element = Tile[elx][ely];
1536 laser.num_edges = start_edge + 1;
1537 if (start_edge == 0)
1538 laser.current_angle = laser.start_angle;
1540 LX = laser.edge[start_edge].x - cSX2;
1541 LY = laser.edge[start_edge].y - cSY2;
1542 XS = 2 * Step[laser.current_angle].x;
1543 YS = 2 * Step[laser.current_angle].y;
1546 Debug("game:mm:DrawLaserExt", "Set (LX, LY) to (%d, %d) [%d]",
1552 if (IS_BEAMER(element) ||
1553 IS_FIBRE_OPTIC(element) ||
1554 IS_PACMAN(element) ||
1555 IS_POLAR(element) ||
1556 IS_POLAR_CROSS(element) ||
1557 element == EL_FUSE_ON)
1562 Debug("game:mm:DrawLaserExt", "element == %d", element);
1565 if (IS_22_5_ANGLE(laser.current_angle)) // neither 90° nor 45° angle
1566 step_size = ((IS_BEAMER(element) || IS_FIBRE_OPTIC(element)) ? 4 : 3);
1570 if (IS_POLAR(element) || IS_POLAR_CROSS(element) ||
1571 ((IS_BEAMER(element) || IS_FIBRE_OPTIC(element)) &&
1572 (laser.num_beamers == 0 ||
1573 start_edge != laser.beamer_edge[laser.num_beamers - 1])))
1575 // element is incoming beamer or other element
1576 step_size = -step_size;
1581 if (IS_BEAMER(element))
1582 Debug("game:mm:DrawLaserExt",
1583 "start_edge == %d, laser.beamer_edge == %d",
1584 start_edge, laser.beamer_edge);
1587 LX += step_size * XS;
1588 LY += step_size * YS;
1590 else if (element != EL_EMPTY)
1599 Debug("game:mm:DrawLaserExt", "Finally: (LX, LY) to (%d, %d) [%d]",
1604 void DrawLaser(int start_edge, int mode)
1606 // do not draw laser if fuse is off
1607 if (laser.fuse_off && mode == DL_LASER_ENABLED)
1610 if (mode == DL_LASER_DISABLED)
1611 DeactivateLaserTargetElement();
1613 if (laser.num_edges - start_edge < 0)
1615 Warn("DrawLaser: laser.num_edges - start_edge < 0");
1620 // check if laser is interrupted by beamer element
1621 if (laser.num_beamers > 0 &&
1622 start_edge < laser.beamer_edge[laser.num_beamers - 1])
1624 if (mode == DL_LASER_ENABLED)
1627 int tmp_start_edge = start_edge;
1629 // draw laser segments forward from the start to the last beamer
1630 for (i = 0; i < laser.num_beamers; i++)
1632 int tmp_num_edges = laser.beamer_edge[i] - tmp_start_edge;
1634 if (tmp_num_edges <= 0)
1638 Debug("game:mm:DrawLaser", "DL_LASER_ENABLED: i==%d: %d, %d",
1639 i, laser.beamer_edge[i], tmp_start_edge);
1642 DrawLaserExt(tmp_start_edge, tmp_num_edges, DL_LASER_ENABLED);
1644 tmp_start_edge = laser.beamer_edge[i];
1647 // draw last segment from last beamer to the end
1648 DrawLaserExt(tmp_start_edge, laser.num_edges - tmp_start_edge,
1654 int last_num_edges = laser.num_edges;
1655 int num_beamers = laser.num_beamers;
1657 // delete laser segments backward from the end to the first beamer
1658 for (i = num_beamers - 1; i >= 0; i--)
1660 int tmp_num_edges = last_num_edges - laser.beamer_edge[i];
1662 if (laser.beamer_edge[i] - start_edge <= 0)
1665 DrawLaserExt(laser.beamer_edge[i], tmp_num_edges, DL_LASER_DISABLED);
1667 last_num_edges = laser.beamer_edge[i];
1668 laser.num_beamers--;
1672 if (last_num_edges - start_edge <= 0)
1673 Debug("game:mm:DrawLaser", "DL_LASER_DISABLED: %d, %d",
1674 last_num_edges, start_edge);
1677 // special case when rotating first beamer: delete laser edge on beamer
1678 // (but do not start scanning on previous edge to prevent mirror sound)
1679 if (last_num_edges - start_edge == 1 && start_edge > 0)
1680 DrawLaserLines(&laser.edge[start_edge - 1], 2, DL_LASER_DISABLED);
1682 // delete first segment from start to the first beamer
1683 DrawLaserExt(start_edge, last_num_edges - start_edge, DL_LASER_DISABLED);
1688 DrawLaserExt(start_edge, laser.num_edges - start_edge, mode);
1691 game_mm.laser_enabled = mode;
1694 void DrawLaser_MM(void)
1696 DrawLaser(0, game_mm.laser_enabled);
1699 static boolean HitElement(int element, int hit_mask)
1701 if (IS_DF_SLOPE(element))
1703 // check if laser scan has crossed element boundaries (not just mini tiles)
1704 boolean cross_x = (getLevelFromLaserX(LX) != getLevelFromLaserX(LX + 2));
1705 boolean cross_y = (getLevelFromLaserY(LY) != getLevelFromLaserY(LY + 2));
1707 // check if an edge was hit while crossing element borders
1708 if (cross_x && cross_y && get_number_of_bits(hit_mask) == 1)
1710 // check both sides of potentially diagonal side of slope
1711 int dx1 = (LX + XS) % TILEX;
1712 int dy1 = (LY + YS) % TILEY;
1713 int dx2 = (LX + XS + 2) % TILEX;
1714 int dy2 = (LY + YS + 2) % TILEY;
1715 int pos = getMaskFromElement(element);
1717 // check if we are entering empty space area after hitting edge
1718 if (mm_masks[pos][dx1 / 2][dy1 / 2] != 'X' &&
1719 mm_masks[pos][dx2 / 2][dy2 / 2] != 'X')
1721 // we already know that we hit an edge, but use this function to go on
1722 if (HitOnlyAnEdge(hit_mask))
1727 int mirrored_angle = get_mirrored_angle(laser.current_angle,
1728 get_element_angle(element));
1729 int opposite_angle = get_opposite_angle(laser.current_angle);
1731 // check if laser is reflected by slope by 180°
1732 if (mirrored_angle == opposite_angle)
1734 AddDamagedField(LX / TILEX, LY / TILEY);
1736 laser.overloaded = TRUE;
1743 if (HitOnlyAnEdge(hit_mask))
1747 if (IS_MOVING(ELX, ELY) || IS_BLOCKED(ELX, ELY))
1748 element = MovingOrBlocked2Element_MM(ELX, ELY);
1751 Debug("game:mm:HitElement", "(1): element == %d", element);
1755 if ((ELX * TILEX + 14 - LX) * YS == (ELY * TILEY + 14 - LY) * XS)
1756 Debug("game:mm:HitElement", "(%d): EXACT MATCH @ (%d, %d)",
1759 Debug("game:mm:HitElement", "(%d): FUZZY MATCH @ (%d, %d)",
1763 AddDamagedField(ELX, ELY);
1765 boolean through_center = ((ELX * TILEX + 14 - LX) * YS ==
1766 (ELY * TILEY + 14 - LY) * XS);
1768 // this is more precise: check if laser would go through the center
1769 if (!IS_DF_SLOPE(element) && !through_center)
1773 // prevent cutting through laser emitter with laser beam
1774 if (IS_LASER(element))
1777 // skip the whole element before continuing the scan
1785 while (ELX == LX/TILEX && ELY == LY/TILEY && LX > 0 && LY > 0);
1787 if ((LX/TILEX > ELX || LY/TILEY > ELY) && skip_count > 1)
1789 /* skipping scan positions to the right and down skips one scan
1790 position too much, because this is only the top left scan position
1791 of totally four scan positions (plus one to the right, one to the
1792 bottom and one to the bottom right) */
1793 /* ... but only roll back scan position if more than one step done */
1803 Debug("game:mm:HitElement", "(2): element == %d", element);
1806 if (LX + 5 * XS < 0 ||
1816 Debug("game:mm:HitElement", "(3): element == %d", element);
1819 if (IS_POLAR(element) &&
1820 ((element - EL_POLAR_START) % 2 ||
1821 (element - EL_POLAR_START) / 2 != laser.current_angle % 8))
1823 PlayLevelSound_MM(ELX, ELY, element, MM_ACTION_HITTING);
1825 laser.num_damages--;
1830 if (IS_POLAR_CROSS(element) &&
1831 (element - EL_POLAR_CROSS_START) != laser.current_angle % 4)
1833 PlayLevelSound_MM(ELX, ELY, element, MM_ACTION_HITTING);
1835 laser.num_damages--;
1840 if (IS_DF_SLOPE(element) && !through_center)
1844 if (hit_mask == HIT_MASK_ALL)
1846 // laser already inside slope -- go back half step
1853 AddLaserEdge(LX, LY);
1855 LX -= (ABS(XS) < ABS(YS) ? correction * SIGN(XS) : 0);
1856 LY -= (ABS(YS) < ABS(XS) ? correction * SIGN(YS) : 0);
1858 else if (!IS_BEAMER(element) &&
1859 !IS_FIBRE_OPTIC(element) &&
1860 !IS_GRID_WOOD(element) &&
1861 element != EL_FUEL_EMPTY)
1864 if ((ELX * TILEX + 14 - LX) * YS == (ELY * TILEY + 14 - LY) * XS)
1865 Debug("game:mm:HitElement", "EXACT MATCH @ (%d, %d)", ELX, ELY);
1867 Debug("game:mm:HitElement", "FUZZY MATCH @ (%d, %d)", ELX, ELY);
1870 LX = ELX * TILEX + 14;
1871 LY = ELY * TILEY + 14;
1873 AddLaserEdge(LX, LY);
1876 if (IS_MIRROR(element) ||
1877 IS_MIRROR_FIXED(element) ||
1878 IS_POLAR(element) ||
1879 IS_POLAR_CROSS(element) ||
1880 IS_DF_MIRROR(element) ||
1881 IS_DF_MIRROR_AUTO(element) ||
1882 IS_DF_MIRROR_FIXED(element) ||
1883 IS_DF_SLOPE(element) ||
1884 element == EL_PRISM ||
1885 element == EL_REFRACTOR)
1887 int current_angle = laser.current_angle;
1890 laser.num_damages--;
1892 AddDamagedField(ELX, ELY);
1894 laser.damage[laser.num_damages - 1].is_mirror = TRUE;
1897 Hit[ELX][ELY] = laser.damage[laser.num_damages - 1].edge;
1899 if (IS_MIRROR(element) ||
1900 IS_MIRROR_FIXED(element) ||
1901 IS_DF_MIRROR(element) ||
1902 IS_DF_MIRROR_AUTO(element) ||
1903 IS_DF_MIRROR_FIXED(element) ||
1904 IS_DF_SLOPE(element))
1905 laser.current_angle = get_mirrored_angle(laser.current_angle,
1906 get_element_angle(element));
1908 if (element == EL_PRISM || element == EL_REFRACTOR)
1909 laser.current_angle = RND(16);
1911 XS = 2 * Step[laser.current_angle].x;
1912 YS = 2 * Step[laser.current_angle].y;
1916 // start from center position for all game elements but slope
1917 if (!IS_22_5_ANGLE(laser.current_angle)) // 90° or 45° angle
1922 LX += step_size * XS;
1923 LY += step_size * YS;
1927 // advance laser position until reaching the next tile (slopes)
1928 while (LX / TILEX == ELX && (LX + 2) / TILEX == ELX &&
1929 LY / TILEY == ELY && (LY + 2) / TILEY == ELY)
1936 // draw sparkles on mirror
1937 if ((IS_MIRROR(element) ||
1938 IS_MIRROR_FIXED(element) ||
1939 element == EL_PRISM) &&
1940 current_angle != laser.current_angle)
1942 MovDelay[ELX][ELY] = 11; // start animation
1945 if ((!IS_POLAR(element) && !IS_POLAR_CROSS(element)) &&
1946 current_angle != laser.current_angle)
1947 PlayLevelSound_MM(ELX, ELY, element, MM_ACTION_HITTING);
1950 (get_opposite_angle(laser.current_angle) ==
1951 laser.damage[laser.num_damages - 1].angle ? TRUE : FALSE);
1953 if (IS_DF_SLOPE(element))
1955 // handle special cases for slope element
1957 if (IS_45_ANGLE(laser.current_angle))
1961 elx = getLevelFromLaserX(LX);
1962 ely = getLevelFromLaserY(LY);
1964 if (IN_LEV_FIELD(elx, ely))
1966 int element_next = Tile[elx][ely];
1968 // check if slope is followed by slope with opposite orientation
1969 if (IS_DF_SLOPE(element_next) && ABS(element - element_next) == 2)
1970 laser.overloaded = TRUE;
1973 int nr = element - EL_DF_SLOPE_START;
1974 int dx = (nr == 0 ? (XS > 0 ? TILEX - 1 : -1) :
1975 nr == 1 ? (XS > 0 ? TILEX : 1) :
1976 nr == 2 ? (XS > 0 ? TILEX : 1) :
1977 nr == 3 ? (XS > 0 ? TILEX - 1 : -1) : 0);
1978 int dy = (nr == 0 ? (YS > 0 ? TILEY - 1 : -1) :
1979 nr == 1 ? (YS > 0 ? TILEY - 1 : -1) :
1980 nr == 2 ? (YS > 0 ? TILEY : 0) :
1981 nr == 3 ? (YS > 0 ? TILEY : 0) : 0);
1983 int px = ELX * TILEX + dx;
1984 int py = ELY * TILEY + dy;
1989 elx = getLevelFromLaserX(px);
1990 ely = getLevelFromLaserY(py);
1992 if (IN_LEV_FIELD(elx, ely))
1994 int element_side = Tile[elx][ely];
1996 // check if end of slope is blocked by other element
1997 if (IS_WALL(element_side) || IS_WALL_CHANGING(element_side))
1999 int pos = dy / MINI_TILEY * 2 + dx / MINI_TILEX;
2001 if (element & (1 << pos))
2002 laser.overloaded = TRUE;
2006 int pos = getMaskFromElement(element_side);
2008 if (mm_masks[pos][dx / 2][dy / 2] == 'X')
2009 laser.overloaded = TRUE;
2015 return (laser.overloaded ? TRUE : FALSE);
2018 if (element == EL_FUEL_FULL)
2020 laser.stops_inside_element = TRUE;
2025 if (element == EL_BOMB || element == EL_MINE || element == EL_GRAY_BALL)
2027 PlayLevelSound_MM(ELX, ELY, element, MM_ACTION_HITTING);
2029 Tile[ELX][ELY] = (element == EL_BOMB ? EL_BOMB_ACTIVE :
2030 element == EL_MINE ? EL_MINE_ACTIVE :
2031 EL_GRAY_BALL_ACTIVE);
2033 GfxFrame[ELX][ELY] = 0; // restart animation
2035 laser.dest_element_last = Tile[ELX][ELY];
2036 laser.dest_element_last_x = ELX;
2037 laser.dest_element_last_y = ELY;
2039 if (element == EL_MINE)
2040 laser.overloaded = TRUE;
2043 if (element == EL_KETTLE ||
2044 element == EL_CELL ||
2045 element == EL_KEY ||
2046 element == EL_LIGHTBALL ||
2047 element == EL_PACMAN ||
2048 IS_PACMAN(element) ||
2049 IS_ENVELOPE(element))
2051 if (!IS_PACMAN(element) &&
2052 !IS_ENVELOPE(element))
2055 if (element == EL_PACMAN)
2058 if (element == EL_KETTLE || element == EL_CELL)
2060 if (game_mm.kettles_still_needed > 0)
2061 game_mm.kettles_still_needed--;
2063 game.snapshot.collected_item = TRUE;
2065 if (game_mm.kettles_still_needed == 0)
2069 DrawLaser(0, DL_LASER_ENABLED);
2072 else if (element == EL_KEY)
2076 else if (IS_PACMAN(element))
2078 DeletePacMan(ELX, ELY);
2080 else if (IS_ENVELOPE(element))
2082 Tile[ELX][ELY] = EL_ENVELOPE_1_OPENING + ENVELOPE_NR(Tile[ELX][ELY]);
2085 RaiseScoreElement_MM(element);
2090 if (element == EL_LIGHTBULB_OFF || element == EL_LIGHTBULB_ON)
2092 PlayLevelSound_MM(ELX, ELY, element, MM_ACTION_HITTING);
2094 DrawLaser(0, DL_LASER_ENABLED);
2096 if (Tile[ELX][ELY] == EL_LIGHTBULB_OFF)
2098 Tile[ELX][ELY] = EL_LIGHTBULB_ON;
2099 game_mm.lights_still_needed--;
2103 Tile[ELX][ELY] = EL_LIGHTBULB_OFF;
2104 game_mm.lights_still_needed++;
2107 DrawField_MM(ELX, ELY);
2108 DrawLaser(0, DL_LASER_ENABLED);
2113 laser.stops_inside_element = TRUE;
2119 Debug("game:mm:HitElement", "(4): element == %d", element);
2122 if ((IS_BEAMER(element) || IS_FIBRE_OPTIC(element)) &&
2123 laser.num_beamers < MAX_NUM_BEAMERS &&
2124 laser.beamer[BEAMER_NR(element)][1].num)
2126 int beamer_angle = get_element_angle(element);
2127 int beamer_nr = BEAMER_NR(element);
2131 Debug("game:mm:HitElement", "(BEAMER): element == %d", element);
2134 laser.num_damages--;
2136 if (IS_FIBRE_OPTIC(element) ||
2137 laser.current_angle == get_opposite_angle(beamer_angle))
2141 LX = ELX * TILEX + 14;
2142 LY = ELY * TILEY + 14;
2144 AddLaserEdge(LX, LY);
2145 AddDamagedField(ELX, ELY);
2147 laser.damage[laser.num_damages - 1].is_mirror = TRUE;
2150 Hit[ELX][ELY] = laser.damage[laser.num_damages - 1].edge;
2152 pos = (ELX == laser.beamer[beamer_nr][0].x &&
2153 ELY == laser.beamer[beamer_nr][0].y ? 1 : 0);
2154 ELX = laser.beamer[beamer_nr][pos].x;
2155 ELY = laser.beamer[beamer_nr][pos].y;
2156 LX = ELX * TILEX + 14;
2157 LY = ELY * TILEY + 14;
2159 if (IS_BEAMER(element))
2161 laser.current_angle = get_element_angle(Tile[ELX][ELY]);
2162 XS = 2 * Step[laser.current_angle].x;
2163 YS = 2 * Step[laser.current_angle].y;
2166 laser.beamer_edge[laser.num_beamers] = laser.num_edges;
2168 AddLaserEdge(LX, LY);
2169 AddDamagedField(ELX, ELY);
2171 laser.damage[laser.num_damages - 1].is_mirror = TRUE;
2174 Hit[ELX][ELY] = laser.damage[laser.num_damages - 1].edge;
2176 if (laser.current_angle == (laser.current_angle >> 1) << 1)
2181 LX += step_size * XS;
2182 LY += step_size * YS;
2184 laser.num_beamers++;
2193 static boolean HitOnlyAnEdge(int hit_mask)
2195 // check if the laser hit only the edge of an element and, if so, go on
2198 Debug("game:mm:HitOnlyAnEdge", "LX, LY, hit_mask == %d, %d, %d",
2202 if ((hit_mask == HIT_MASK_TOPLEFT ||
2203 hit_mask == HIT_MASK_TOPRIGHT ||
2204 hit_mask == HIT_MASK_BOTTOMLEFT ||
2205 hit_mask == HIT_MASK_BOTTOMRIGHT) &&
2206 laser.current_angle % 4) // angle is not 90°
2210 if (hit_mask == HIT_MASK_TOPLEFT)
2215 else if (hit_mask == HIT_MASK_TOPRIGHT)
2220 else if (hit_mask == HIT_MASK_BOTTOMLEFT)
2225 else // (hit_mask == HIT_MASK_BOTTOMRIGHT)
2231 AddDamagedField((LX + 2 * dx) / TILEX, (LY + 2 * dy) / TILEY);
2237 Debug("game:mm:HitOnlyAnEdge", "[HitOnlyAnEdge() == TRUE]");
2244 Debug("game:mm:HitOnlyAnEdge", "[HitOnlyAnEdge() == FALSE]");
2250 static boolean HitPolarizer(int element, int hit_mask)
2252 if (HitOnlyAnEdge(hit_mask))
2255 if (IS_DF_GRID(element))
2257 int grid_angle = get_element_angle(element);
2260 Debug("game:mm:HitPolarizer", "angle: grid == %d, laser == %d",
2261 grid_angle, laser.current_angle);
2264 AddLaserEdge(LX, LY);
2265 AddDamagedField(ELX, ELY);
2268 Hit[ELX][ELY] = laser.damage[laser.num_damages - 1].edge;
2270 if (laser.current_angle == grid_angle ||
2271 laser.current_angle == get_opposite_angle(grid_angle))
2273 // skip the whole element before continuing the scan
2279 while (ELX == LX/TILEX && ELY == LY/TILEY && LX > 0 && LY > 0);
2281 if (LX/TILEX > ELX || LY/TILEY > ELY)
2283 /* skipping scan positions to the right and down skips one scan
2284 position too much, because this is only the top left scan position
2285 of totally four scan positions (plus one to the right, one to the
2286 bottom and one to the bottom right) */
2292 AddLaserEdge(LX, LY);
2298 Debug("game:mm:HitPolarizer", "LX, LY == %d, %d [%d, %d] [%d, %d]",
2300 LX / TILEX, LY / TILEY,
2301 LX % TILEX, LY % TILEY);
2306 else if (IS_GRID_STEEL_FIXED(element) || IS_GRID_STEEL_AUTO(element))
2308 return HitReflectingWalls(element, hit_mask);
2312 return HitAbsorbingWalls(element, hit_mask);
2315 else if (IS_GRID_STEEL(element))
2317 // may be required if graphics for steel grid redefined
2318 AddDamagedField(ELX, ELY);
2320 return HitReflectingWalls(element, hit_mask);
2322 else // IS_GRID_WOOD
2324 // may be required if graphics for wooden grid redefined
2325 AddDamagedField(ELX, ELY);
2327 return HitAbsorbingWalls(element, hit_mask);
2333 static boolean HitBlock(int element, int hit_mask)
2335 boolean check = FALSE;
2337 if ((element == EL_GATE_STONE || element == EL_GATE_WOOD) &&
2338 game_mm.num_keys == 0)
2341 if (element == EL_BLOCK_STONE || element == EL_BLOCK_WOOD)
2344 int ex = ELX * TILEX + 14;
2345 int ey = ELY * TILEY + 14;
2349 for (i = 1; i < 32; i++)
2354 if ((x == ex || x == ex + 1) && (y == ey || y == ey + 1))
2359 if (check && (element == EL_BLOCK_WOOD || element == EL_GATE_WOOD))
2360 return HitAbsorbingWalls(element, hit_mask);
2364 AddLaserEdge(LX - XS, LY - YS);
2365 AddDamagedField(ELX, ELY);
2368 Box[ELX][ELY] = laser.num_edges;
2370 return HitReflectingWalls(element, hit_mask);
2373 if (element == EL_GATE_STONE || element == EL_GATE_WOOD)
2375 int xs = XS / 2, ys = YS / 2;
2377 if ((hit_mask & HIT_MASK_DIAGONAL_1) == HIT_MASK_DIAGONAL_1 ||
2378 (hit_mask & HIT_MASK_DIAGONAL_2) == HIT_MASK_DIAGONAL_2)
2380 laser.overloaded = (element == EL_GATE_STONE);
2385 if (ABS(xs) == 1 && ABS(ys) == 1 &&
2386 (hit_mask == HIT_MASK_TOP ||
2387 hit_mask == HIT_MASK_LEFT ||
2388 hit_mask == HIT_MASK_RIGHT ||
2389 hit_mask == HIT_MASK_BOTTOM))
2390 AddDamagedField(ELX - xs * (hit_mask == HIT_MASK_TOP ||
2391 hit_mask == HIT_MASK_BOTTOM),
2392 ELY - ys * (hit_mask == HIT_MASK_LEFT ||
2393 hit_mask == HIT_MASK_RIGHT));
2394 AddLaserEdge(LX, LY);
2400 if (element == EL_GATE_STONE && Box[ELX][ELY])
2402 DrawLaser(Box[ELX][ELY] - 1, DL_LASER_DISABLED);
2414 if (element == EL_BLOCK_STONE || element == EL_BLOCK_WOOD)
2416 int xs = XS / 2, ys = YS / 2;
2418 if ((hit_mask & HIT_MASK_DIAGONAL_1) == HIT_MASK_DIAGONAL_1 ||
2419 (hit_mask & HIT_MASK_DIAGONAL_2) == HIT_MASK_DIAGONAL_2)
2421 laser.overloaded = (element == EL_BLOCK_STONE);
2426 if (ABS(xs) == 1 && ABS(ys) == 1 &&
2427 (hit_mask == HIT_MASK_TOP ||
2428 hit_mask == HIT_MASK_LEFT ||
2429 hit_mask == HIT_MASK_RIGHT ||
2430 hit_mask == HIT_MASK_BOTTOM))
2431 AddDamagedField(ELX - xs * (hit_mask == HIT_MASK_TOP ||
2432 hit_mask == HIT_MASK_BOTTOM),
2433 ELY - ys * (hit_mask == HIT_MASK_LEFT ||
2434 hit_mask == HIT_MASK_RIGHT));
2435 AddDamagedField(ELX, ELY);
2437 LX = ELX * TILEX + 14;
2438 LY = ELY * TILEY + 14;
2440 AddLaserEdge(LX, LY);
2442 laser.stops_inside_element = TRUE;
2450 static boolean HitLaserSource(int element, int hit_mask)
2452 if (HitOnlyAnEdge(hit_mask))
2455 PlayLevelSound_MM(ELX, ELY, element, MM_ACTION_HITTING);
2457 laser.overloaded = TRUE;
2462 static boolean HitLaserDestination(int element, int hit_mask)
2464 if (HitOnlyAnEdge(hit_mask))
2467 if (element != EL_EXIT_OPEN &&
2468 !(IS_RECEIVER(element) &&
2469 game_mm.kettles_still_needed == 0 &&
2470 laser.current_angle == get_opposite_angle(get_element_angle(element))))
2472 PlayLevelSound_MM(ELX, ELY, element, MM_ACTION_HITTING);
2477 if (IS_RECEIVER(element) ||
2478 (IS_22_5_ANGLE(laser.current_angle) &&
2479 (ELX != (LX + 6 * XS) / TILEX ||
2480 ELY != (LY + 6 * YS) / TILEY ||
2489 LX = ELX * TILEX + 14;
2490 LY = ELY * TILEY + 14;
2492 laser.stops_inside_element = TRUE;
2495 AddLaserEdge(LX, LY);
2496 AddDamagedField(ELX, ELY);
2498 if (game_mm.lights_still_needed == 0)
2500 game_mm.level_solved = TRUE;
2502 SetTileCursorActive(FALSE);
2508 static boolean HitReflectingWalls(int element, int hit_mask)
2510 // check if laser hits side of a wall with an angle that is not 90°
2511 if (!IS_90_ANGLE(laser.current_angle) && (hit_mask == HIT_MASK_TOP ||
2512 hit_mask == HIT_MASK_LEFT ||
2513 hit_mask == HIT_MASK_RIGHT ||
2514 hit_mask == HIT_MASK_BOTTOM))
2516 PlayLevelSound_MM(ELX, ELY, element, MM_ACTION_HITTING);
2521 if (!IS_DF_GRID(element))
2522 AddLaserEdge(LX, LY);
2524 // check if laser hits wall with an angle of 45°
2525 if (!IS_22_5_ANGLE(laser.current_angle))
2527 if (hit_mask == HIT_MASK_TOP || hit_mask == HIT_MASK_BOTTOM)
2530 laser.current_angle = get_mirrored_angle(laser.current_angle,
2533 else // hit_mask == HIT_MASK_LEFT || hit_mask == HIT_MASK_RIGHT
2536 laser.current_angle = get_mirrored_angle(laser.current_angle,
2540 AddLaserEdge(LX, LY);
2542 XS = 2 * Step[laser.current_angle].x;
2543 YS = 2 * Step[laser.current_angle].y;
2547 else if (hit_mask == HIT_MASK_TOP || hit_mask == HIT_MASK_BOTTOM)
2549 laser.current_angle = get_mirrored_angle(laser.current_angle,
2554 if (!IS_DF_GRID(element))
2555 AddLaserEdge(LX, LY);
2560 if (!IS_DF_GRID(element))
2561 AddLaserEdge(LX, LY + YS / 2);
2564 if (!IS_DF_GRID(element))
2565 AddLaserEdge(LX, LY);
2568 YS = 2 * Step[laser.current_angle].y;
2572 else // hit_mask == HIT_MASK_LEFT || hit_mask == HIT_MASK_RIGHT
2574 laser.current_angle = get_mirrored_angle(laser.current_angle,
2579 if (!IS_DF_GRID(element))
2580 AddLaserEdge(LX, LY);
2585 if (!IS_DF_GRID(element))
2586 AddLaserEdge(LX + XS / 2, LY);
2589 if (!IS_DF_GRID(element))
2590 AddLaserEdge(LX, LY);
2593 XS = 2 * Step[laser.current_angle].x;
2599 // reflection at the edge of reflecting DF style wall
2600 if (IS_DF_WALL_STEEL(element) && IS_22_5_ANGLE(laser.current_angle))
2602 if (((laser.current_angle == 1 || laser.current_angle == 3) &&
2603 hit_mask == HIT_MASK_TOPRIGHT) ||
2604 ((laser.current_angle == 5 || laser.current_angle == 7) &&
2605 hit_mask == HIT_MASK_TOPLEFT) ||
2606 ((laser.current_angle == 9 || laser.current_angle == 11) &&
2607 hit_mask == HIT_MASK_BOTTOMLEFT) ||
2608 ((laser.current_angle == 13 || laser.current_angle == 15) &&
2609 hit_mask == HIT_MASK_BOTTOMRIGHT))
2612 (hit_mask == HIT_MASK_TOPRIGHT || hit_mask == HIT_MASK_BOTTOMLEFT ?
2613 ANG_MIRROR_135 : ANG_MIRROR_45);
2615 PlayLevelSound_MM(ELX, ELY, element, MM_ACTION_HITTING);
2617 AddDamagedField(ELX, ELY);
2618 AddLaserEdge(LX, LY);
2620 laser.current_angle = get_mirrored_angle(laser.current_angle,
2628 AddLaserEdge(LX, LY);
2634 // reflection inside an edge of reflecting DF style wall
2635 if (IS_DF_WALL_STEEL(element) && IS_22_5_ANGLE(laser.current_angle))
2637 if (((laser.current_angle == 1 || laser.current_angle == 3) &&
2638 hit_mask == (HIT_MASK_ALL ^ HIT_MASK_BOTTOMLEFT)) ||
2639 ((laser.current_angle == 5 || laser.current_angle == 7) &&
2640 hit_mask == (HIT_MASK_ALL ^ HIT_MASK_BOTTOMRIGHT)) ||
2641 ((laser.current_angle == 9 || laser.current_angle == 11) &&
2642 hit_mask == (HIT_MASK_ALL ^ HIT_MASK_TOPRIGHT)) ||
2643 ((laser.current_angle == 13 || laser.current_angle == 15) &&
2644 hit_mask == (HIT_MASK_ALL ^ HIT_MASK_TOPLEFT)))
2647 (hit_mask == (HIT_MASK_ALL ^ HIT_MASK_BOTTOMLEFT) ||
2648 hit_mask == (HIT_MASK_ALL ^ HIT_MASK_TOPRIGHT) ?
2649 ANG_MIRROR_135 : ANG_MIRROR_45);
2651 PlayLevelSound_MM(ELX, ELY, element, MM_ACTION_HITTING);
2654 AddDamagedField(ELX, ELY);
2657 AddLaserEdge(LX - XS, LY - YS);
2658 AddLaserEdge(LX - XS + (ABS(XS) == 4 ? XS/2 : 0),
2659 LY - YS + (ABS(YS) == 4 ? YS/2 : 0));
2661 laser.current_angle = get_mirrored_angle(laser.current_angle,
2669 AddLaserEdge(LX, LY);
2675 // check if laser hits DF style wall with an angle of 90°
2676 if (IS_DF_WALL(element) && IS_90_ANGLE(laser.current_angle))
2678 if ((IS_HORIZ_ANGLE(laser.current_angle) &&
2679 (!(hit_mask & HIT_MASK_TOP) || !(hit_mask & HIT_MASK_BOTTOM))) ||
2680 (IS_VERT_ANGLE(laser.current_angle) &&
2681 (!(hit_mask & HIT_MASK_LEFT) || !(hit_mask & HIT_MASK_RIGHT))))
2683 // laser at last step touched nothing or the same side of the wall
2684 if (LX != last_LX || LY != last_LY || hit_mask == last_hit_mask)
2686 AddDamagedField(ELX, ELY);
2693 last_hit_mask = hit_mask;
2700 if (!HitOnlyAnEdge(hit_mask))
2702 laser.overloaded = TRUE;
2710 static boolean HitAbsorbingWalls(int element, int hit_mask)
2712 if (HitOnlyAnEdge(hit_mask))
2716 (hit_mask == HIT_MASK_LEFT || hit_mask == HIT_MASK_RIGHT))
2718 AddLaserEdge(LX - XS, LY - YS);
2725 (hit_mask == HIT_MASK_TOP || hit_mask == HIT_MASK_BOTTOM))
2727 AddLaserEdge(LX - XS, LY - YS);
2733 if (IS_WALL_WOOD(element) ||
2734 IS_DF_WALL_WOOD(element) ||
2735 IS_GRID_WOOD(element) ||
2736 IS_GRID_WOOD_FIXED(element) ||
2737 IS_GRID_WOOD_AUTO(element) ||
2738 element == EL_FUSE_ON ||
2739 element == EL_BLOCK_WOOD ||
2740 element == EL_GATE_WOOD)
2742 PlayLevelSound_MM(ELX, ELY, element, MM_ACTION_HITTING);
2747 if (IS_WALL_ICE(element))
2753 // check if laser hit adjacent edges of two diagonal tiles
2754 if (ELX != lx / TILEX)
2756 if (ELY != ly / TILEY)
2759 mask = lx / MINI_TILEX - ELX * 2 + 1; // Quadrant (horizontal)
2760 mask <<= ((ly / MINI_TILEY - ELY * 2) > 0 ? 2 : 0); // || (vertical)
2762 // check if laser hits wall with an angle of 90°
2763 if (IS_90_ANGLE(laser.current_angle))
2764 mask += mask * (2 + IS_HORIZ_ANGLE(laser.current_angle) * 2);
2766 if (mask == 1 || mask == 2 || mask == 4 || mask == 8)
2770 for (i = 0; i < 4; i++)
2772 if (mask == (1 << i) && (XS > 0) == (i % 2) && (YS > 0) == (i / 2))
2773 mask = 15 - (8 >> i);
2774 else if (ABS(XS) == 4 &&
2776 (XS > 0) == (i % 2) &&
2777 (YS < 0) == (i / 2))
2778 mask = 3 + (i / 2) * 9;
2779 else if (ABS(YS) == 4 &&
2781 (XS < 0) == (i % 2) &&
2782 (YS > 0) == (i / 2))
2783 mask = 5 + (i % 2) * 5;
2787 laser.wall_mask = mask;
2789 else if (IS_WALL_AMOEBA(element))
2791 int elx = (LX - 2 * XS) / TILEX;
2792 int ely = (LY - 2 * YS) / TILEY;
2793 int element2 = Tile[elx][ely];
2796 if (element2 != EL_EMPTY && !IS_WALL_AMOEBA(element2))
2798 laser.dest_element = EL_EMPTY;
2806 mask = (LX - 2 * XS) / 16 - ELX * 2 + 1;
2807 mask <<= ((LY - 2 * YS) / 16 - ELY * 2) * 2;
2809 if (IS_90_ANGLE(laser.current_angle))
2810 mask += mask * (2 + IS_HORIZ_ANGLE(laser.current_angle) * 2);
2812 laser.dest_element = element2 | EL_WALL_AMOEBA_BASE;
2814 laser.wall_mask = mask;
2820 static void OpenExit(int x, int y)
2824 if (!MovDelay[x][y]) // next animation frame
2825 MovDelay[x][y] = 4 * delay;
2827 if (MovDelay[x][y]) // wait some time before next frame
2832 phase = MovDelay[x][y] / delay;
2834 if (!(MovDelay[x][y] % delay) && IN_SCR_FIELD(x, y))
2835 DrawGraphicAnimation_MM(x, y, IMG_MM_EXIT_OPENING, 3 - phase);
2837 if (!MovDelay[x][y])
2839 Tile[x][y] = EL_EXIT_OPEN;
2845 static void OpenGrayBall(int x, int y)
2849 if (!MovDelay[x][y]) // next animation frame
2851 if (IS_WALL(Store[x][y]))
2853 DrawWalls_MM(x, y, Store[x][y]);
2855 // copy wall tile to spare bitmap for "melting" animation
2856 BlitBitmap(drawto_mm, bitmap_db_field, cSX + x * TILEX, cSY + y * TILEY,
2857 TILEX, TILEY, x * TILEX, y * TILEY);
2859 DrawElement_MM(x, y, EL_GRAY_BALL);
2862 MovDelay[x][y] = 50 * delay;
2865 if (MovDelay[x][y]) // wait some time before next frame
2869 if (!(MovDelay[x][y] % delay) && IN_SCR_FIELD(x, y))
2873 int dx = RND(26), dy = RND(26);
2875 if (IS_WALL(Store[x][y]))
2877 // copy wall tile from spare bitmap for "melting" animation
2878 bitmap = bitmap_db_field;
2884 int graphic = el2gfx(Store[x][y]);
2886 getGraphicSource(graphic, 0, &bitmap, &gx, &gy);
2889 BlitBitmap(bitmap, drawto_mm, gx + dx, gy + dy, 6, 6,
2890 cSX + x * TILEX + dx, cSY + y * TILEY + dy);
2892 laser.redraw = TRUE;
2894 MarkTileDirty(x, y);
2897 if (!MovDelay[x][y])
2899 Tile[x][y] = Store[x][y];
2900 Store[x][y] = Store2[x][y] = 0;
2901 MovDir[x][y] = MovPos[x][y] = MovDelay[x][y] = 0;
2903 InitField(x, y, FALSE);
2906 ScanLaser_FromLastMirror();
2911 static void OpenEnvelope(int x, int y)
2913 int num_frames = 8; // seven frames plus final empty space
2915 if (!MovDelay[x][y]) // next animation frame
2916 MovDelay[x][y] = num_frames;
2918 if (MovDelay[x][y]) // wait some time before next frame
2920 int nr = ENVELOPE_OPENING_NR(Tile[x][y]);
2924 if (MovDelay[x][y] > 0 && IN_SCR_FIELD(x, y))
2926 int graphic = el_act2gfx(EL_ENVELOPE_1 + nr, MM_ACTION_COLLECTING);
2927 int frame = num_frames - MovDelay[x][y] - 1;
2929 DrawGraphicAnimation_MM(x, y, graphic, frame);
2931 laser.redraw = TRUE;
2934 if (MovDelay[x][y] == 0)
2936 Tile[x][y] = EL_EMPTY;
2947 static void MeltIce(int x, int y)
2952 if (!MovDelay[x][y]) // next animation frame
2953 MovDelay[x][y] = frames * delay;
2955 if (MovDelay[x][y]) // wait some time before next frame
2958 int wall_mask = Store2[x][y];
2959 int real_element = Tile[x][y] - EL_WALL_CHANGING_BASE + EL_WALL_ICE_BASE;
2962 phase = frames - MovDelay[x][y] / delay - 1;
2964 if (!MovDelay[x][y])
2966 Tile[x][y] = real_element & (wall_mask ^ 0xFF);
2967 Store[x][y] = Store2[x][y] = 0;
2969 DrawWalls_MM(x, y, Tile[x][y]);
2971 if (Tile[x][y] == EL_WALL_ICE_BASE)
2972 Tile[x][y] = EL_EMPTY;
2974 ScanLaser_FromLastMirror();
2976 else if (!(MovDelay[x][y] % delay) && IN_SCR_FIELD(x, y))
2978 DrawWallsAnimation_MM(x, y, real_element, phase, wall_mask);
2980 laser.redraw = TRUE;
2985 static void GrowAmoeba(int x, int y)
2990 if (!MovDelay[x][y]) // next animation frame
2991 MovDelay[x][y] = frames * delay;
2993 if (MovDelay[x][y]) // wait some time before next frame
2996 int wall_mask = Store2[x][y];
2997 int real_element = Tile[x][y] - EL_WALL_CHANGING_BASE + EL_WALL_AMOEBA_BASE;
3000 phase = MovDelay[x][y] / delay;
3002 if (!MovDelay[x][y])
3004 Tile[x][y] = real_element;
3005 Store[x][y] = Store2[x][y] = 0;
3007 DrawWalls_MM(x, y, Tile[x][y]);
3008 DrawLaser(0, DL_LASER_ENABLED);
3010 else if (!(MovDelay[x][y] % delay) && IN_SCR_FIELD(x, y))
3012 DrawWallsAnimation_MM(x, y, real_element, phase, wall_mask);
3017 static void DrawFieldAnimated_MM(int x, int y)
3021 laser.redraw = TRUE;
3024 static void DrawFieldAnimatedIfNeeded_MM(int x, int y)
3026 int element = Tile[x][y];
3027 int graphic = el2gfx(element);
3029 if (!getGraphicInfo_NewFrame(x, y, graphic))
3034 laser.redraw = TRUE;
3037 static void DrawFieldTwinkle(int x, int y)
3039 if (MovDelay[x][y] != 0) // wait some time before next frame
3045 if (MovDelay[x][y] != 0)
3047 int graphic = IMG_TWINKLE_WHITE;
3048 int frame = getGraphicAnimationFrame(graphic, 10 - MovDelay[x][y]);
3050 DrawGraphicThruMask_MM(SCREENX(x), SCREENY(y), graphic, frame);
3053 laser.redraw = TRUE;
3057 static void Explode_MM(int x, int y, int phase, int mode)
3059 int num_phase = 9, delay = 2;
3060 int last_phase = num_phase * delay;
3061 int half_phase = (num_phase / 2) * delay;
3064 laser.redraw = TRUE;
3066 if (phase == EX_PHASE_START) // initialize 'Store[][]' field
3068 center_element = Tile[x][y];
3070 if (IS_MOVING(x, y) || IS_BLOCKED(x, y))
3072 // put moving element to center field (and let it explode there)
3073 center_element = MovingOrBlocked2Element_MM(x, y);
3074 RemoveMovingField_MM(x, y);
3076 Tile[x][y] = center_element;
3079 if (center_element != EL_GRAY_BALL_ACTIVE)
3080 Store[x][y] = EL_EMPTY;
3081 Store2[x][y] = center_element;
3083 Tile[x][y] = EL_EXPLODING_OPAQUE;
3085 GfxElement[x][y] = (center_element == EL_BOMB_ACTIVE ? EL_BOMB :
3086 center_element == EL_GRAY_BALL_ACTIVE ? EL_GRAY_BALL :
3087 IS_MCDUFFIN(center_element) ? EL_MCDUFFIN :
3090 MovDir[x][y] = MovPos[x][y] = MovDelay[x][y] = 0;
3092 ExplodePhase[x][y] = 1;
3098 GfxFrame[x][y] = 0; // restart explosion animation
3100 ExplodePhase[x][y] = (phase < last_phase ? phase + 1 : 0);
3102 center_element = Store2[x][y];
3104 if (phase == half_phase && Store[x][y] == EL_EMPTY)
3106 Tile[x][y] = EL_EXPLODING_TRANSP;
3108 if (x == ELX && y == ELY)
3112 if (phase == last_phase)
3114 if (center_element == EL_BOMB_ACTIVE)
3116 DrawLaser(0, DL_LASER_DISABLED);
3119 Bang_MM(laser.start_edge.x, laser.start_edge.y);
3121 laser.overloaded = FALSE;
3123 else if (IS_MCDUFFIN(center_element) || IS_LASER(center_element))
3125 GameOver_MM(GAME_OVER_BOMB);
3128 Tile[x][y] = Store[x][y];
3130 Store[x][y] = Store2[x][y] = 0;
3131 MovDir[x][y] = MovPos[x][y] = MovDelay[x][y] = 0;
3133 InitField(x, y, FALSE);
3136 if (center_element == EL_GRAY_BALL_ACTIVE)
3137 ScanLaser_FromLastMirror();
3139 else if (!(phase % delay) && IN_SCR_FIELD(SCREENX(x), SCREENY(y)))
3141 int graphic = el_act2gfx(GfxElement[x][y], MM_ACTION_EXPLODING);
3142 int frame = getGraphicAnimationFrameXY(graphic, x, y);
3144 DrawGraphicAnimation_MM(x, y, graphic, frame);
3146 MarkTileDirty(x, y);
3150 static void Bang_MM(int x, int y)
3152 int element = Tile[x][y];
3154 if (IS_PACMAN(element))
3155 PlayLevelSound_MM(x, y, element, MM_ACTION_EXPLODING);
3156 else if (element == EL_BOMB_ACTIVE || IS_MCDUFFIN(element))
3157 PlayLevelSound_MM(x, y, element, MM_ACTION_EXPLODING);
3158 else if (element == EL_KEY)
3159 PlayLevelSound_MM(x, y, element, MM_ACTION_EXPLODING);
3161 PlayLevelSound_MM(x, y, element, MM_ACTION_EXPLODING);
3163 Explode_MM(x, y, EX_PHASE_START, EX_TYPE_NORMAL);
3166 static void TurnRound(int x, int y)
3178 { 0, 0 }, { 0, 0 }, { 0, 0 },
3183 int left, right, back;
3187 { MV_DOWN, MV_UP, MV_RIGHT },
3188 { MV_UP, MV_DOWN, MV_LEFT },
3190 { MV_LEFT, MV_RIGHT, MV_DOWN },
3194 { MV_RIGHT, MV_LEFT, MV_UP }
3197 int element = Tile[x][y];
3198 int old_move_dir = MovDir[x][y];
3199 int right_dir = turn[old_move_dir].right;
3200 int back_dir = turn[old_move_dir].back;
3201 int right_dx = move_xy[right_dir].x, right_dy = move_xy[right_dir].y;
3202 int right_x = x + right_dx, right_y = y + right_dy;
3204 if (element == EL_PACMAN)
3206 boolean can_turn_right = FALSE;
3208 if (IN_LEV_FIELD(right_x, right_y) &&
3209 IS_EATABLE4PACMAN(Tile[right_x][right_y]))
3210 can_turn_right = TRUE;
3213 MovDir[x][y] = right_dir;
3215 MovDir[x][y] = back_dir;
3221 static void StartMoving_MM(int x, int y)
3223 int element = Tile[x][y];
3228 if (CAN_MOVE(element))
3232 if (MovDelay[x][y]) // wait some time before next movement
3240 // now make next step
3242 Moving2Blocked(x, y, &newx, &newy); // get next screen position
3244 if (element == EL_PACMAN &&
3245 IN_LEV_FIELD(newx, newy) && IS_EATABLE4PACMAN(Tile[newx][newy]) &&
3246 !ObjHit(newx, newy, HIT_POS_CENTER))
3248 Store[newx][newy] = Tile[newx][newy];
3249 Tile[newx][newy] = EL_EMPTY;
3251 DrawField_MM(newx, newy);
3253 else if (!IN_LEV_FIELD(newx, newy) || !IS_FREE(newx, newy) ||
3254 ObjHit(newx, newy, HIT_POS_CENTER))
3256 // object was running against a wall
3263 InitMovingField_MM(x, y, MovDir[x][y]);
3267 ContinueMoving_MM(x, y);
3270 static void ContinueMoving_MM(int x, int y)
3272 int element = Tile[x][y];
3273 int direction = MovDir[x][y];
3274 int dx = (direction == MV_LEFT ? -1 : direction == MV_RIGHT ? +1 : 0);
3275 int dy = (direction == MV_UP ? -1 : direction == MV_DOWN ? +1 : 0);
3276 int horiz_move = (dx!=0);
3277 int newx = x + dx, newy = y + dy;
3278 int step = (horiz_move ? dx : dy) * TILEX / 8;
3280 MovPos[x][y] += step;
3282 if (ABS(MovPos[x][y]) >= TILEX) // object reached its destination
3284 Tile[x][y] = EL_EMPTY;
3285 Tile[newx][newy] = element;
3287 MovPos[x][y] = MovDir[x][y] = MovDelay[x][y] = 0;
3288 MovDelay[newx][newy] = 0;
3290 if (!CAN_MOVE(element))
3291 MovDir[newx][newy] = 0;
3294 DrawField_MM(newx, newy);
3296 Stop[newx][newy] = TRUE;
3298 if (element == EL_PACMAN)
3300 if (Store[newx][newy] == EL_BOMB)
3301 Bang_MM(newx, newy);
3303 if (IS_WALL_AMOEBA(Store[newx][newy]) &&
3304 (LX + 2 * XS) / TILEX == newx &&
3305 (LY + 2 * YS) / TILEY == newy)
3312 else // still moving on
3317 laser.redraw = TRUE;
3320 boolean ClickElement(int x, int y, int button)
3322 static DelayCounter click_delay = { CLICK_DELAY };
3323 static boolean new_button = TRUE;
3324 boolean element_clicked = FALSE;
3329 // initialize static variables
3330 click_delay.count = 0;
3331 click_delay.value = CLICK_DELAY;
3337 // do not rotate objects hit by the laser after the game was solved
3338 if (game_mm.level_solved && Hit[x][y])
3341 if (button == MB_RELEASED)
3344 click_delay.value = CLICK_DELAY;
3346 // release eventually hold auto-rotating mirror
3347 RotateMirror(x, y, MB_RELEASED);
3352 if (!FrameReached(&click_delay) && !new_button)
3355 if (button == MB_MIDDLEBUTTON) // middle button has no function
3358 if (!IN_LEV_FIELD(x, y))
3361 if (Tile[x][y] == EL_EMPTY)
3364 element = Tile[x][y];
3366 if (IS_MIRROR(element) ||
3367 IS_BEAMER(element) ||
3368 IS_POLAR(element) ||
3369 IS_POLAR_CROSS(element) ||
3370 IS_DF_MIRROR(element) ||
3371 IS_DF_MIRROR_AUTO(element))
3373 RotateMirror(x, y, button);
3375 element_clicked = TRUE;
3377 else if (IS_MCDUFFIN(element))
3379 boolean has_laser = (x == laser.start_edge.x && y == laser.start_edge.y);
3381 if (has_laser && !laser.fuse_off)
3382 DrawLaser(0, DL_LASER_DISABLED);
3384 element = get_rotated_element(element, BUTTON_ROTATION(button));
3386 Tile[x][y] = element;
3391 laser.start_angle = get_element_angle(element);
3395 if (!laser.fuse_off)
3399 element_clicked = TRUE;
3401 else if (element == EL_FUSE_ON && laser.fuse_off)
3403 if (x != laser.fuse_x || y != laser.fuse_y)
3406 laser.fuse_off = FALSE;
3407 laser.fuse_x = laser.fuse_y = -1;
3409 DrawGraphic_MM(x, y, IMG_MM_FUSE_ACTIVE);
3412 element_clicked = TRUE;
3414 else if (element == EL_FUSE_ON && !laser.fuse_off && new_button)
3416 laser.fuse_off = TRUE;
3419 laser.overloaded = FALSE;
3421 DrawLaser(0, DL_LASER_DISABLED);
3422 DrawGraphic_MM(x, y, IMG_MM_FUSE);
3424 element_clicked = TRUE;
3426 else if (element == EL_LIGHTBALL)
3429 RaiseScoreElement_MM(element);
3430 DrawLaser(0, DL_LASER_ENABLED);
3432 element_clicked = TRUE;
3434 else if (IS_ENVELOPE(element))
3436 Tile[x][y] = EL_ENVELOPE_1_OPENING + ENVELOPE_NR(element);
3438 element_clicked = TRUE;
3441 click_delay.value = (new_button ? CLICK_DELAY_FIRST : CLICK_DELAY);
3444 return element_clicked;
3447 static void RotateMirror(int x, int y, int button)
3449 if (button == MB_RELEASED)
3451 // release eventually hold auto-rotating mirror
3458 if (IS_MIRROR(Tile[x][y]) ||
3459 IS_POLAR_CROSS(Tile[x][y]) ||
3460 IS_POLAR(Tile[x][y]) ||
3461 IS_BEAMER(Tile[x][y]) ||
3462 IS_DF_MIRROR(Tile[x][y]) ||
3463 IS_GRID_STEEL_AUTO(Tile[x][y]) ||
3464 IS_GRID_WOOD_AUTO(Tile[x][y]))
3466 Tile[x][y] = get_rotated_element(Tile[x][y], BUTTON_ROTATION(button));
3468 else if (IS_DF_MIRROR_AUTO(Tile[x][y]))
3470 if (button == MB_LEFTBUTTON)
3472 // left mouse button only for manual adjustment, no auto-rotating;
3473 // freeze mirror for until mouse button released
3477 else if (button == MB_RIGHTBUTTON && (hold_x != x || hold_y != y))
3479 Tile[x][y] = get_rotated_element(Tile[x][y], ROTATE_RIGHT);
3483 if (IS_GRID_STEEL_AUTO(Tile[x][y]) || IS_GRID_WOOD_AUTO(Tile[x][y]))
3485 int edge = Hit[x][y];
3491 DrawLaser(edge - 1, DL_LASER_DISABLED);
3495 else if (ObjHit(x, y, HIT_POS_CENTER))
3497 int edge = Hit[x][y];
3501 Warn("RotateMirror: inconsistent field Hit[][]!\n");
3506 DrawLaser(edge - 1, DL_LASER_DISABLED);
3513 if (ObjHit(x, y, HIT_POS_EDGE | HIT_POS_BETWEEN))
3518 if ((IS_BEAMER(Tile[x][y]) ||
3519 IS_POLAR(Tile[x][y]) ||
3520 IS_POLAR_CROSS(Tile[x][y])) && x == ELX && y == ELY)
3522 if (IS_BEAMER(Tile[x][y]))
3525 Debug("game:mm:RotateMirror", "TEST (%d, %d) [%d] [%d]",
3526 LX, LY, laser.beamer_edge, laser.beamer[1].num);
3539 DrawLaser(0, DL_LASER_ENABLED);
3543 static void AutoRotateMirrors(void)
3547 if (!FrameReached(&rotate_delay))
3550 for (x = 0; x < lev_fieldx; x++)
3552 for (y = 0; y < lev_fieldy; y++)
3554 int element = Tile[x][y];
3556 // do not rotate objects hit by the laser after the game was solved
3557 if (game_mm.level_solved && Hit[x][y])
3560 if (IS_DF_MIRROR_AUTO(element) ||
3561 IS_GRID_WOOD_AUTO(element) ||
3562 IS_GRID_STEEL_AUTO(element) ||
3563 element == EL_REFRACTOR)
3565 RotateMirror(x, y, MB_RIGHTBUTTON);
3567 laser.redraw = TRUE;
3573 static boolean ObjHit(int obx, int oby, int bits)
3580 if (bits & HIT_POS_CENTER)
3582 if (CheckLaserPixel(cSX + obx + 15,
3587 if (bits & HIT_POS_EDGE)
3589 for (i = 0; i < 4; i++)
3590 if (CheckLaserPixel(cSX + obx + 31 * (i % 2),
3591 cSY + oby + 31 * (i / 2)))
3595 if (bits & HIT_POS_BETWEEN)
3597 for (i = 0; i < 4; i++)
3598 if (CheckLaserPixel(cSX + 4 + obx + 22 * (i % 2),
3599 cSY + 4 + oby + 22 * (i / 2)))
3606 static void DeletePacMan(int px, int py)
3612 if (game_mm.num_pacman <= 1)
3614 game_mm.num_pacman = 0;
3618 for (i = 0; i < game_mm.num_pacman; i++)
3619 if (game_mm.pacman[i].x == px && game_mm.pacman[i].y == py)
3622 game_mm.num_pacman--;
3624 for (j = i; j < game_mm.num_pacman; j++)
3626 game_mm.pacman[j].x = game_mm.pacman[j + 1].x;
3627 game_mm.pacman[j].y = game_mm.pacman[j + 1].y;
3628 game_mm.pacman[j].dir = game_mm.pacman[j + 1].dir;
3632 static void GameActions_MM_Ext(void)
3639 for (y = 0; y < lev_fieldy; y++) for (x = 0; x < lev_fieldx; x++)
3642 for (y = 0; y < lev_fieldy; y++) for (x = 0; x < lev_fieldx; x++)
3644 element = Tile[x][y];
3646 if (!IS_MOVING(x, y) && CAN_MOVE(element))
3647 StartMoving_MM(x, y);
3648 else if (IS_MOVING(x, y))
3649 ContinueMoving_MM(x, y);
3650 else if (IS_EXPLODING(element))
3651 Explode_MM(x, y, ExplodePhase[x][y], EX_TYPE_NORMAL);
3652 else if (element == EL_EXIT_OPENING)
3654 else if (element == EL_GRAY_BALL_OPENING)
3656 else if (IS_ENVELOPE_OPENING(element))
3658 else if (IS_WALL_CHANGING(element) && Store[x][y] == EL_WALL_ICE_BASE)
3660 else if (IS_WALL_CHANGING(element) && Store[x][y] == EL_WALL_AMOEBA_BASE)
3662 else if (IS_MIRROR(element) ||
3663 IS_MIRROR_FIXED(element) ||
3664 element == EL_PRISM)
3665 DrawFieldTwinkle(x, y);
3666 else if (element == EL_GRAY_BALL_ACTIVE ||
3667 element == EL_BOMB_ACTIVE ||
3668 element == EL_MINE_ACTIVE)
3669 DrawFieldAnimated_MM(x, y);
3670 else if (!IS_BLOCKED(x, y))
3671 DrawFieldAnimatedIfNeeded_MM(x, y);
3674 AutoRotateMirrors();
3677 // !!! CHANGE THIS: REDRAW ONLY WHEN NEEDED !!!
3679 // redraw after Explode_MM() ...
3681 DrawLaser(0, DL_LASER_ENABLED);
3682 laser.redraw = FALSE;
3687 if (game_mm.num_pacman && FrameReached(&pacman_delay))
3691 if (laser.num_damages > MAX_LASER_LEN && !laser.fuse_off)
3693 DrawLaser(0, DL_LASER_DISABLED);
3698 // skip all following game actions if game is over
3699 if (game_mm.game_over)
3702 if (game_mm.energy_left == 0 && !game.no_level_time_limit && game.time_limit)
3706 GameOver_MM(GAME_OVER_NO_ENERGY);
3711 if (FrameReached(&energy_delay))
3713 if (game_mm.energy_left > 0)
3714 game_mm.energy_left--;
3716 // when out of energy, wait another frame to play "out of time" sound
3719 element = laser.dest_element;
3722 if (element != Tile[ELX][ELY])
3724 Debug("game:mm:GameActions_MM_Ext", "element == %d, Tile[ELX][ELY] == %d",
3725 element, Tile[ELX][ELY]);
3729 if (!laser.overloaded && laser.overload_value == 0 &&
3730 element != EL_BOMB &&
3731 element != EL_BOMB_ACTIVE &&
3732 element != EL_MINE &&
3733 element != EL_MINE_ACTIVE &&
3734 element != EL_GRAY_BALL &&
3735 element != EL_GRAY_BALL_ACTIVE &&
3736 element != EL_BLOCK_STONE &&
3737 element != EL_BLOCK_WOOD &&
3738 element != EL_FUSE_ON &&
3739 element != EL_FUEL_FULL &&
3740 !IS_WALL_ICE(element) &&
3741 !IS_WALL_AMOEBA(element))
3744 overload_delay.value = HEALTH_DELAY(laser.overloaded);
3746 if (((laser.overloaded && laser.overload_value < MAX_LASER_OVERLOAD) ||
3747 (!laser.overloaded && laser.overload_value > 0)) &&
3748 FrameReached(&overload_delay))
3750 if (laser.overloaded)
3751 laser.overload_value++;
3753 laser.overload_value--;
3755 if (game_mm.cheat_no_overload)
3757 laser.overloaded = FALSE;
3758 laser.overload_value = 0;
3761 game_mm.laser_overload_value = laser.overload_value;
3763 if (laser.overload_value < MAX_LASER_OVERLOAD - 8)
3765 SetLaserColor(0xFF);
3767 DrawLaser(0, DL_LASER_ENABLED);
3770 if (!laser.overloaded)
3771 StopSound_MM(SND_MM_GAME_HEALTH_CHARGING);
3772 else if (setup.sound_loops)
3773 PlaySoundLoop_MM(SND_MM_GAME_HEALTH_CHARGING);
3775 PlaySound_MM(SND_MM_GAME_HEALTH_CHARGING);
3777 if (laser.overload_value == MAX_LASER_OVERLOAD)
3779 UpdateAndDisplayGameControlValues();
3783 GameOver_MM(GAME_OVER_OVERLOADED);
3794 if (element == EL_BOMB && CT > native_mm_level.time_bomb)
3796 if (game_mm.cheat_no_explosion)
3801 laser.dest_element = EL_EXPLODING_OPAQUE;
3806 if (element == EL_FUSE_ON && CT > native_mm_level.time_fuse)
3808 laser.fuse_off = TRUE;
3812 DrawLaser(0, DL_LASER_DISABLED);
3813 DrawGraphic_MM(ELX, ELY, IMG_MM_FUSE);
3816 if (element == EL_GRAY_BALL && CT > native_mm_level.time_ball)
3818 if (!Store2[ELX][ELY]) // check if content element not yet determined
3820 int last_anim_random_frame = gfx.anim_random_frame;
3823 if (native_mm_level.ball_choice_mode == ANIM_RANDOM)
3824 gfx.anim_random_frame = RND(native_mm_level.num_ball_contents);
3826 element_pos = getAnimationFrame(native_mm_level.num_ball_contents, 1,
3827 native_mm_level.ball_choice_mode, 0,
3828 game_mm.ball_choice_pos);
3830 if (native_mm_level.ball_choice_mode == ANIM_RANDOM)
3831 gfx.anim_random_frame = last_anim_random_frame;
3833 game_mm.ball_choice_pos++;
3835 int new_element = native_mm_level.ball_content[element_pos];
3836 int new_element_base = map_wall_to_base_element(new_element);
3838 if (IS_WALL(new_element_base))
3840 // always use completely filled wall element
3841 new_element = new_element_base | 0x000f;
3843 else if (native_mm_level.rotate_ball_content &&
3844 get_num_elements(new_element) > 1)
3846 // randomly rotate newly created game element
3847 new_element = get_rotated_element(new_element, RND(16));
3850 Store[ELX][ELY] = new_element;
3851 Store2[ELX][ELY] = TRUE;
3854 if (native_mm_level.explode_ball)
3857 Tile[ELX][ELY] = EL_GRAY_BALL_OPENING;
3859 laser.dest_element = laser.dest_element_last = Tile[ELX][ELY];
3864 if (IS_WALL_ICE(element) && CT > 50)
3866 PlayLevelSound_MM(ELX, ELY, element, MM_ACTION_SHRINKING);
3868 Tile[ELX][ELY] = Tile[ELX][ELY] - EL_WALL_ICE_BASE + EL_WALL_CHANGING_BASE;
3869 Store[ELX][ELY] = EL_WALL_ICE_BASE;
3870 Store2[ELX][ELY] = laser.wall_mask;
3872 laser.dest_element = Tile[ELX][ELY];
3877 if (IS_WALL_AMOEBA(element) && CT > 60)
3880 int element2 = Tile[ELX][ELY];
3882 if (element2 != EL_EMPTY && !IS_WALL_AMOEBA(element))
3885 for (i = laser.num_damages - 1; i >= 0; i--)
3886 if (laser.damage[i].is_mirror)
3889 r = laser.num_edges;
3890 d = laser.num_damages;
3897 DrawLaser(laser.damage[k1].edge - 1, DL_LASER_DISABLED);
3900 DrawLaser(0, DL_LASER_ENABLED);
3903 x = laser.damage[k1].x;
3904 y = laser.damage[k1].y;
3909 for (i = 0; i < 4; i++)
3911 if (laser.wall_mask & (1 << i))
3913 if (CheckLaserPixel(cSX + ELX * TILEX + 14 + (i % 2) * 2,
3914 cSY + ELY * TILEY + 31 * (i / 2)))
3917 if (CheckLaserPixel(cSX + ELX * TILEX + 31 * (i % 2),
3918 cSY + ELY * TILEY + 14 + (i / 2) * 2))
3925 for (i = 0; i < 4; i++)
3927 if (laser.wall_mask & (1 << i))
3929 if (CheckLaserPixel(cSX + ELX * TILEX + 31 * (i % 2),
3930 cSY + ELY * TILEY + 31 * (i / 2)))
3937 if (laser.num_beamers > 0 ||
3938 k1 < 1 || k2 < 4 || k3 < 4 ||
3939 CheckLaserPixel(cSX + ELX * TILEX + 14,
3940 cSY + ELY * TILEY + 14))
3942 laser.num_edges = r;
3943 laser.num_damages = d;
3945 DrawLaser(0, DL_LASER_DISABLED);
3948 Tile[ELX][ELY] = element | laser.wall_mask;
3950 int x = ELX, y = ELY;
3951 int wall_mask = laser.wall_mask;
3954 DrawLaser(0, DL_LASER_ENABLED);
3956 PlayLevelSound_MM(x, y, element, MM_ACTION_GROWING);
3958 Tile[x][y] = Tile[x][y] - EL_WALL_AMOEBA_BASE + EL_WALL_CHANGING_BASE;
3959 Store[x][y] = EL_WALL_AMOEBA_BASE;
3960 Store2[x][y] = wall_mask;
3965 if ((element == EL_BLOCK_WOOD || element == EL_BLOCK_STONE) &&
3966 laser.stops_inside_element && CT > native_mm_level.time_block)
3971 if (ABS(XS) > ABS(YS))
3978 for (i = 0; i < 4; i++)
3985 x = ELX + Step[k * 4].x;
3986 y = ELY + Step[k * 4].y;
3988 if (!IN_LEV_FIELD(x, y) || Tile[x][y] != EL_EMPTY)
3991 if (ObjHit(x, y, HIT_POS_CENTER | HIT_POS_EDGE | HIT_POS_BETWEEN))
3999 laser.overloaded = (element == EL_BLOCK_STONE);
4004 PlayLevelSound_MM(ELX, ELY, element, MM_ACTION_PUSHING);
4007 Tile[x][y] = element;
4009 DrawGraphic_MM(ELX, ELY, IMG_EMPTY);
4012 if (element == EL_BLOCK_STONE && Box[ELX][ELY])
4014 DrawLaser(Box[ELX][ELY] - 1, DL_LASER_DISABLED);
4015 DrawLaser(laser.num_edges - 1, DL_LASER_ENABLED);
4023 if (element == EL_FUEL_FULL && CT > 10)
4025 int num_init_game_frames = INIT_GAME_ACTIONS_DELAY;
4026 int start = num_init_game_frames * game_mm.energy_left / native_mm_level.time;
4028 for (i = start; i <= num_init_game_frames; i++)
4030 if (i == num_init_game_frames)
4031 StopSound_MM(SND_MM_GAME_LEVELTIME_CHARGING);
4032 else if (setup.sound_loops)
4033 PlaySoundLoop_MM(SND_MM_GAME_LEVELTIME_CHARGING);
4035 PlaySound_MM(SND_MM_GAME_LEVELTIME_CHARGING);
4037 game_mm.energy_left = native_mm_level.time * i / num_init_game_frames;
4039 UpdateAndDisplayGameControlValues();
4044 Tile[ELX][ELY] = laser.dest_element = EL_FUEL_EMPTY;
4046 DrawField_MM(ELX, ELY);
4048 DrawLaser(0, DL_LASER_ENABLED);
4054 void GameActions_MM(struct MouseActionInfo action)
4056 boolean element_clicked = ClickElement(action.lx, action.ly, action.button);
4057 boolean button_released = (action.button == MB_RELEASED);
4059 GameActions_MM_Ext();
4061 CheckSingleStepMode_MM(element_clicked, button_released);
4064 static void MovePacMen(void)
4066 int mx, my, ox, oy, nx, ny;
4070 if (++pacman_nr >= game_mm.num_pacman)
4073 game_mm.pacman[pacman_nr].dir--;
4075 for (l = 1; l < 5; l++)
4077 game_mm.pacman[pacman_nr].dir++;
4079 if (game_mm.pacman[pacman_nr].dir > 4)
4080 game_mm.pacman[pacman_nr].dir = 1;
4082 if (game_mm.pacman[pacman_nr].dir % 2)
4085 my = game_mm.pacman[pacman_nr].dir - 2;
4090 mx = 3 - game_mm.pacman[pacman_nr].dir;
4093 ox = game_mm.pacman[pacman_nr].x;
4094 oy = game_mm.pacman[pacman_nr].y;
4097 element = Tile[nx][ny];
4099 if (nx < 0 || nx > 15 || ny < 0 || ny > 11)
4102 if (!IS_EATABLE4PACMAN(element))
4105 if (ObjHit(nx, ny, HIT_POS_CENTER))
4108 Tile[ox][oy] = EL_EMPTY;
4110 EL_PACMAN_RIGHT - 1 +
4111 (game_mm.pacman[pacman_nr].dir - 1 +
4112 (game_mm.pacman[pacman_nr].dir % 2) * 2);
4114 game_mm.pacman[pacman_nr].x = nx;
4115 game_mm.pacman[pacman_nr].y = ny;
4117 DrawGraphic_MM(ox, oy, IMG_EMPTY);
4119 if (element != EL_EMPTY)
4121 int graphic = el2gfx(Tile[nx][ny]);
4126 getGraphicSource(graphic, 0, &bitmap, &src_x, &src_y);
4129 ox = cSX + ox * TILEX;
4130 oy = cSY + oy * TILEY;
4132 for (i = 1; i < 33; i += 2)
4133 BlitBitmap(bitmap, window,
4134 src_x, src_y, TILEX, TILEY,
4135 ox + i * mx, oy + i * my);
4136 Ct = Ct + FrameCounter - CT;
4139 DrawField_MM(nx, ny);
4142 if (!laser.fuse_off)
4144 DrawLaser(0, DL_LASER_ENABLED);
4146 if (ObjHit(nx, ny, HIT_POS_BETWEEN))
4148 AddDamagedField(nx, ny);
4150 laser.damage[laser.num_damages - 1].edge = 0;
4154 if (element == EL_BOMB)
4155 DeletePacMan(nx, ny);
4157 if (IS_WALL_AMOEBA(element) &&
4158 (LX + 2 * XS) / TILEX == nx &&
4159 (LY + 2 * YS) / TILEY == ny)
4169 static void InitMovingField_MM(int x, int y, int direction)
4171 int newx = x + (direction == MV_LEFT ? -1 : direction == MV_RIGHT ? +1 : 0);
4172 int newy = y + (direction == MV_UP ? -1 : direction == MV_DOWN ? +1 : 0);
4174 MovDir[x][y] = direction;
4175 MovDir[newx][newy] = direction;
4177 if (Tile[newx][newy] == EL_EMPTY)
4178 Tile[newx][newy] = EL_BLOCKED;
4181 static int MovingOrBlocked2Element_MM(int x, int y)
4183 int element = Tile[x][y];
4185 if (element == EL_BLOCKED)
4189 Blocked2Moving(x, y, &oldx, &oldy);
4191 return Tile[oldx][oldy];
4197 static void RemoveMovingField_MM(int x, int y)
4199 int oldx = x, oldy = y, newx = x, newy = y;
4201 if (Tile[x][y] != EL_BLOCKED && !IS_MOVING(x, y))
4204 if (IS_MOVING(x, y))
4206 Moving2Blocked(x, y, &newx, &newy);
4207 if (Tile[newx][newy] != EL_BLOCKED)
4210 else if (Tile[x][y] == EL_BLOCKED)
4212 Blocked2Moving(x, y, &oldx, &oldy);
4213 if (!IS_MOVING(oldx, oldy))
4217 Tile[oldx][oldy] = EL_EMPTY;
4218 Tile[newx][newy] = EL_EMPTY;
4219 MovPos[oldx][oldy] = MovDir[oldx][oldy] = MovDelay[oldx][oldy] = 0;
4220 MovPos[newx][newy] = MovDir[newx][newy] = MovDelay[newx][newy] = 0;
4222 DrawLevelField_MM(oldx, oldy);
4223 DrawLevelField_MM(newx, newy);
4226 static void RaiseScore_MM(int value)
4228 game_mm.score += value;
4231 void RaiseScoreElement_MM(int element)
4236 case EL_PACMAN_RIGHT:
4238 case EL_PACMAN_LEFT:
4239 case EL_PACMAN_DOWN:
4240 RaiseScore_MM(native_mm_level.score[SC_PACMAN]);
4244 RaiseScore_MM(native_mm_level.score[SC_KEY]);
4249 RaiseScore_MM(native_mm_level.score[SC_COLLECTIBLE]);
4253 RaiseScore_MM(native_mm_level.score[SC_LIGHTBALL]);
4262 // ----------------------------------------------------------------------------
4263 // Mirror Magic game engine snapshot handling functions
4264 // ----------------------------------------------------------------------------
4266 void SaveEngineSnapshotValues_MM(void)
4270 engine_snapshot_mm.game_mm = game_mm;
4271 engine_snapshot_mm.laser = laser;
4273 for (x = 0; x < MAX_PLAYFIELD_WIDTH; x++)
4275 for (y = 0; y < MAX_PLAYFIELD_HEIGHT; y++)
4277 engine_snapshot_mm.Ur[x][y] = Ur[x][y];
4278 engine_snapshot_mm.Hit[x][y] = Hit[x][y];
4279 engine_snapshot_mm.Box[x][y] = Box[x][y];
4280 engine_snapshot_mm.Angle[x][y] = Angle[x][y];
4284 engine_snapshot_mm.LX = LX;
4285 engine_snapshot_mm.LY = LY;
4286 engine_snapshot_mm.XS = XS;
4287 engine_snapshot_mm.YS = YS;
4288 engine_snapshot_mm.ELX = ELX;
4289 engine_snapshot_mm.ELY = ELY;
4290 engine_snapshot_mm.CT = CT;
4291 engine_snapshot_mm.Ct = Ct;
4293 engine_snapshot_mm.last_LX = last_LX;
4294 engine_snapshot_mm.last_LY = last_LY;
4295 engine_snapshot_mm.last_hit_mask = last_hit_mask;
4296 engine_snapshot_mm.hold_x = hold_x;
4297 engine_snapshot_mm.hold_y = hold_y;
4298 engine_snapshot_mm.pacman_nr = pacman_nr;
4300 engine_snapshot_mm.rotate_delay = rotate_delay;
4301 engine_snapshot_mm.pacman_delay = pacman_delay;
4302 engine_snapshot_mm.energy_delay = energy_delay;
4303 engine_snapshot_mm.overload_delay = overload_delay;
4306 void LoadEngineSnapshotValues_MM(void)
4310 // stored engine snapshot buffers already restored at this point
4312 game_mm = engine_snapshot_mm.game_mm;
4313 laser = engine_snapshot_mm.laser;
4315 for (x = 0; x < MAX_PLAYFIELD_WIDTH; x++)
4317 for (y = 0; y < MAX_PLAYFIELD_HEIGHT; y++)
4319 Ur[x][y] = engine_snapshot_mm.Ur[x][y];
4320 Hit[x][y] = engine_snapshot_mm.Hit[x][y];
4321 Box[x][y] = engine_snapshot_mm.Box[x][y];
4322 Angle[x][y] = engine_snapshot_mm.Angle[x][y];
4326 LX = engine_snapshot_mm.LX;
4327 LY = engine_snapshot_mm.LY;
4328 XS = engine_snapshot_mm.XS;
4329 YS = engine_snapshot_mm.YS;
4330 ELX = engine_snapshot_mm.ELX;
4331 ELY = engine_snapshot_mm.ELY;
4332 CT = engine_snapshot_mm.CT;
4333 Ct = engine_snapshot_mm.Ct;
4335 last_LX = engine_snapshot_mm.last_LX;
4336 last_LY = engine_snapshot_mm.last_LY;
4337 last_hit_mask = engine_snapshot_mm.last_hit_mask;
4338 hold_x = engine_snapshot_mm.hold_x;
4339 hold_y = engine_snapshot_mm.hold_y;
4340 pacman_nr = engine_snapshot_mm.pacman_nr;
4342 rotate_delay = engine_snapshot_mm.rotate_delay;
4343 pacman_delay = engine_snapshot_mm.pacman_delay;
4344 energy_delay = engine_snapshot_mm.energy_delay;
4345 overload_delay = engine_snapshot_mm.overload_delay;
4347 RedrawPlayfield_MM();
4350 static int getAngleFromTouchDelta(int dx, int dy, int base)
4352 double pi = 3.141592653;
4353 double rad = atan2((double)-dy, (double)dx);
4354 double rad2 = (rad < 0 ? rad + 2 * pi : rad);
4355 double deg = rad2 * 180.0 / pi;
4357 return (int)(deg * base / 360.0 + 0.5) % base;
4360 int getButtonFromTouchPosition(int x, int y, int dst_mx, int dst_my)
4362 // calculate start (source) position to be at the middle of the tile
4363 int src_mx = cSX + x * TILESIZE_VAR + TILESIZE_VAR / 2;
4364 int src_my = cSY + y * TILESIZE_VAR + TILESIZE_VAR / 2;
4365 int dx = dst_mx - src_mx;
4366 int dy = dst_my - src_my;
4375 if (!IN_LEV_FIELD(x, y))
4378 element = Tile[x][y];
4380 if (!IS_MCDUFFIN(element) &&
4381 !IS_MIRROR(element) &&
4382 !IS_BEAMER(element) &&
4383 !IS_POLAR(element) &&
4384 !IS_POLAR_CROSS(element) &&
4385 !IS_DF_MIRROR(element))
4388 angle_old = get_element_angle(element);
4390 if (IS_MCDUFFIN(element))
4392 angle_new = (dx > 0 && ABS(dy) < ABS(dx) ? ANG_RAY_RIGHT :
4393 dy < 0 && ABS(dx) < ABS(dy) ? ANG_RAY_UP :
4394 dx < 0 && ABS(dy) < ABS(dx) ? ANG_RAY_LEFT :
4395 dy > 0 && ABS(dx) < ABS(dy) ? ANG_RAY_DOWN :
4398 else if (IS_MIRROR(element) ||
4399 IS_DF_MIRROR(element))
4401 for (i = 0; i < laser.num_damages; i++)
4403 if (laser.damage[i].x == x &&
4404 laser.damage[i].y == y &&
4405 ObjHit(x, y, HIT_POS_CENTER))
4407 angle_old = get_mirrored_angle(laser.damage[i].angle, angle_old);
4408 angle_new = getAngleFromTouchDelta(dx, dy, base) % phases;
4415 if (angle_new == -1)
4417 if (IS_MIRROR(element) ||
4418 IS_DF_MIRROR(element) ||
4422 if (IS_POLAR_CROSS(element))
4425 angle_new = getAngleFromTouchDelta(dx, dy, base) % phases;
4428 button = (angle_new == angle_old ? 0 :
4429 (angle_new - angle_old + phases) % phases < (phases / 2) ?
4430 MB_LEFTBUTTON : MB_RIGHTBUTTON);