302 asset_root(resolveAssetRoot(path)),
303 puzzle_asset_root(resolvePuzzleAssetRoot(resolveAssetRoot(path))),
304 high_scores(scorePath(asset_root)),
305 frame_width(framebuffer.width),
306 frame_height(framebuffer.height) {
310 const std::string
font_path = dataPath(
"font.ttf");
318 tryOpenFirstGamepad();
326 if (e.type == SDL_EVENT_QUIT) {
331 if (e.type == SDL_EVENT_GAMEPAD_ADDED) {
332 openGamepad(e.gdevice.which);
336 if (e.type == SDL_EVENT_GAMEPAD_REMOVED) {
337 if (gamepad !=
nullptr && e.gdevice.which == gamepadId) {
339 tryOpenFirstGamepad();
344 if (e.type == SDL_EVENT_GAMEPAD_BUTTON_DOWN) {
345 handleControllerButton(e.gbutton.button);
350 handleNameText(e.text.text);
354 if (e.type != SDL_EVENT_KEY_DOWN) {
360 if (isConfirmKey(e.key.key) || e.key.key == SDLK_ESCAPE) {
365 handleMenuKey(e.key.key);
368 handleGameKey(e.key.key);
372 if (e.key.key == SDLK_RETURN || e.key.key == SDLK_ESCAPE) {
377 handleNameKey(e.key.key);
383 const Uint64 now = SDL_GetTicks();
384 layout = computeLayout();
385 updateGridViewInput(now);
390 if (!drawIntro(now)) {
425 static constexpr std::array<const char *, menu_item_count> menu_files{{
427 "menu_high_scores.png",
432 std::filesystem::path asset_root;
433 std::filesystem::path puzzle_asset_root;
437 std::mt19937 rng{std::random_device{}()};
438 std::array<std::array<Cell, board_cols>,
board_rows> board{};
440 mxvk::Font title_font{};
441 mxvk::Font ui_font{};
442 mxvk::VK_Sprite *background_intro =
nullptr;
443 mxvk::VK_Sprite *mxvk_logo =
nullptr;
444 mxvk::VK_Sprite *background_menu =
nullptr;
445 mxvk::VK_Sprite *background_game =
nullptr;
446 mxvk::VK_Sprite *cursor =
nullptr;
447 std::array<mxvk::VK_Sprite *, menu_files.size()> menu_items{};
448 mxvk::VK_Sprite *panel =
nullptr;
449 mxvk::VK_Sprite *overlay =
nullptr;
451 const SDL_PixelFormatDetails *frame_format =
nullptr;
452 mxvk::VK_Sprite *frame_sprite =
nullptr;
453 int frame_width = 1280;
454 int frame_height = 720;
455 std::string player_name;
456 int menu_selection = 0;
460 int lines_toward_speedup = 0;
461 int fall_delay_ms = 520;
462 Uint64 intro_start_ms = 0;
463 Uint64 last_update_ms = 0;
464 Uint64 fall_accumulator_ms = 0;
465 Uint64 last_view_update_ms = 0;
466 float grid_pitch = 0.0f;
467 float grid_yaw = 0.0f;
469 Uint32 joy_repeat_left_ms = 0;
470 Uint32 joy_repeat_right_ms = 0;
471 Uint32 joy_repeat_up_ms = 0;
472 Uint32 joy_repeat_down_ms = 0;
473 SDL_Gamepad *gamepad =
nullptr;
474 SDL_JoystickID gamepadId = 0;
476 bool awaiting_name =
false;
477 bool score_added =
false;
478 bool waiting_for_spawn =
false;
480 static bool isConfirmKey(SDL_Keycode key) {
481 return key == SDLK_RETURN || key == SDLK_SPACE;
484 static int scaled(
int value,
float scale) {
485 return std::max(1,
static_cast<int>(std::lround(
static_cast<float>(value) * scale)));
488 static int scaledPos(
int value,
float scale) {
489 return static_cast<int>(std::lround(
static_cast<float>(value) * scale));
492 static int flashingSpriteIndex(
int x,
int y, Uint64 now) {
493 const Uint64 tick = now / 18U;
494 const Uint64 mixed = tick +
static_cast<Uint64
>(x * 37 + y * 101);
495 return static_cast<int>((mixed % 9U) + 1U);
498 [[nodiscard]] Layout computeLayout()
const {
501 result.width = extent.width == 0U ?
base_width :
static_cast<int>(extent.width);
502 result.height = extent.height == 0U ?
base_height :
static_cast<int>(extent.height);
503 result.scale_x =
static_cast<float>(result.width) /
static_cast<float>(base_width);
504 result.scale_y =
static_cast<float>(result.height) /
static_cast<float>(base_height);
505 result.game_scale = std::min(result.scale_x, result.scale_y);
506 result.game_w = scaled(base_width, result.game_scale);
507 result.game_h = scaled(base_height, result.game_scale);
508 result.game_x = (result.width - result.game_w) / 2;
509 result.game_y = (result.height - result.game_h) / 2;
510 result.board_x = result.game_x + scaledPos(185, result.game_scale);
511 result.board_y = result.game_y + scaledPos(95, result.game_scale);
512 result.cell_w = scaled(32, result.game_scale);
513 result.cell_h = scaled(16, result.game_scale);
514 result.next_x = result.game_x + scaledPos(510, result.game_scale);
515 result.next_y = result.game_y + scaledPos(200, result.game_scale);
516 result.menu_x = scaled(505, result.scale_x);
517 result.menu_y = scaled(400, result.scale_y);
518 result.menu_w = scaled(430, result.scale_x);
519 result.menu_h = scaled(82, result.scale_y);
520 result.menu_step = scaled(118, result.scale_y);
524 std::string dataPath(
const char *name)
const {
525 return (asset_root /
"data" / name).string();
528 std::string puzzleDataPath(
const char *name)
const {
529 const std::filesystem::path shared_path = puzzle_asset_root /
"data" / name;
530 if (std::filesystem::exists(shared_path)) {
531 return shared_path.string();
533 return dataPath(name);
536 mxvk::VK_Sprite *loadPngSprite(
const char *name) {
540 mxvk::VK_Sprite *loadEffectSprite(
const char *name) {
541 return createSprite(dataPath(name),
"", dataPath(
"intro.frag.spv"));
544 mxvk::VK_Sprite *makeSolidPixel(std::uint8_t r, std::uint8_t g, std::uint8_t b, std::uint8_t a) {
545 const std::array<std::uint8_t, 4> pixel{r, g, b, a};
552 background_intro = loadEffectSprite(
"intro.png");
553 background_menu = loadEffectSprite(
"start.png");
555 mxvk_logo =
createSprite(puzzleDataPath(
"mxvk_logo.png"));
556 cursor = loadPngSprite(
"cursor.png");
558 for (std::size_t i = 0; i < menu_files.size(); ++i) {
559 menu_items[i] = loadPngSprite(menu_files[i]);
562 panel = makeSolidPixel(0, 0, 0, 192);
563 overlay = makeSolidPixel(0, 0, 0, 128);
566 void setScreen(Screen next) {
568 SDL_StopTextInput(
window.get());
576 SDL_StartTextInput(
window.get());
581 intro_start_ms = SDL_GetTicks();
586 for (
auto &row : board) {
587 for (Cell &cell : row) {
595 lines_toward_speedup = 0;
598 fall_accumulator_ms = 0;
600 awaiting_name =
false;
602 waiting_for_spawn =
false;
605 piece.next_colors = randomColors();
609 std::array<int, piece_height> randomColors() {
610 std::uniform_int_distribution<int> dist(1, 9);
611 std::array<int, piece_height> colors{dist(rng), dist(rng), dist(rng)};
612 while (colors[0] == colors[1] && colors[1] == colors[2]) {
613 colors[1] = dist(rng);
618 bool canPlacePiece(
int x,
int y)
const {
619 if (x < 0 || x >= board_cols) {
624 const int row = y + i;
625 if (row < 0 || row >= board_rows) {
628 if (board[
static_cast<std::size_t
>(row)][
static_cast<std::size_t
>(x)].color != 0) {
635 bool movePiece(
int dx,
int dy) {
636 const int next_x = piece.x + dx;
637 const int next_y = piece.y + dy;
638 if (!canPlacePiece(next_x, next_y)) {
648 while (movePiece(0, 1)) {
650 lockPiece(SDL_GetTicks());
653 void rotatePieceColors(
bool forward) {
655 const int temp = piece.colors.back();
656 piece.colors[2] = piece.colors[1];
657 piece.colors[1] = piece.colors[0];
658 piece.colors[0] = temp;
660 const int temp = piece.colors.front();
661 piece.colors[0] = piece.colors[1];
662 piece.colors[1] = piece.colors[2];
663 piece.colors[2] = temp;
667 void handleControllerButton(Uint8 button) {
670 if (button == SDL_GAMEPAD_BUTTON_SOUTH || button == SDL_GAMEPAD_BUTTON_START) {
675 if (button == SDL_GAMEPAD_BUTTON_DPAD_UP) {
676 menu_selection = (menu_selection +
menu_item_count - 1) % menu_item_count;
677 }
else if (button == SDL_GAMEPAD_BUTTON_DPAD_DOWN) {
678 menu_selection = (menu_selection + 1) % menu_item_count;
679 }
else if (button == SDL_GAMEPAD_BUTTON_SOUTH || button == SDL_GAMEPAD_BUTTON_START) {
680 handleMenuSelection();
681 }
else if (button == SDL_GAMEPAD_BUTTON_EAST || button == SDL_GAMEPAD_BUTTON_BACK) {
686 if (button == SDL_GAMEPAD_BUTTON_BACK) {
688 }
else if (button == SDL_GAMEPAD_BUTTON_START) {
690 }
else if (paused || awaiting_name) {
692 }
else if (button == SDL_GAMEPAD_BUTTON_DPAD_LEFT) {
694 }
else if (button == SDL_GAMEPAD_BUTTON_DPAD_RIGHT) {
696 }
else if (button == SDL_GAMEPAD_BUTTON_DPAD_DOWN) {
697 if (!movePiece(0, 1)) {
698 lockPiece(SDL_GetTicks());
700 }
else if (button == SDL_GAMEPAD_BUTTON_DPAD_UP || button == SDL_GAMEPAD_BUTTON_SOUTH) {
701 rotatePieceColors(
true);
702 }
else if (button == SDL_GAMEPAD_BUTTON_EAST) {
703 rotatePieceColors(
false);
704 }
else if (button == SDL_GAMEPAD_BUTTON_NORTH) {
710 if (button == SDL_GAMEPAD_BUTTON_SOUTH || button == SDL_GAMEPAD_BUTTON_START) {
712 }
else if (button == SDL_GAMEPAD_BUTTON_EAST || button == SDL_GAMEPAD_BUTTON_BACK) {
714 SDL_StopTextInput(
window.get());
717 }
else if (button == SDL_GAMEPAD_BUTTON_SOUTH || button == SDL_GAMEPAD_BUTTON_START ||
718 button == SDL_GAMEPAD_BUTTON_EAST || button == SDL_GAMEPAD_BUTTON_BACK) {
723 if (button == SDL_GAMEPAD_BUTTON_SOUTH || button == SDL_GAMEPAD_BUTTON_START ||
724 button == SDL_GAMEPAD_BUTTON_EAST || button == SDL_GAMEPAD_BUTTON_BACK) {
729 if (button == SDL_GAMEPAD_BUTTON_SOUTH || button == SDL_GAMEPAD_BUTTON_START) {
731 }
else if (button == SDL_GAMEPAD_BUTTON_EAST || button == SDL_GAMEPAD_BUTTON_BACK) {
733 SDL_StopTextInput(
window.get());
740 void handleMenuSelection() {
741 switch (menu_selection) {
760 void handleMenuKey(SDL_Keycode key) {
761 if (key == SDLK_ESCAPE) {
766 if (key == SDLK_UP) {
767 menu_selection = (menu_selection +
menu_item_count - 1) % menu_item_count;
771 if (key == SDLK_DOWN) {
772 menu_selection = (menu_selection + 1) % menu_item_count;
776 if (!isConfirmKey(key)) {
780 handleMenuSelection();
783 void handleGameKey(SDL_Keycode key) {
784 if (key == SDLK_ESCAPE) {
789 if (key ==
'p' || key ==
'P') {
794 if (paused || awaiting_name) {
798 if (key == SDLK_LEFT) {
800 }
else if (key == SDLK_RIGHT) {
802 }
else if (key == SDLK_DOWN) {
803 if (!movePiece(0, 1)) {
804 lockPiece(SDL_GetTicks());
806 }
else if (key == SDLK_UP) {
807 rotatePieceColors(
true);
808 }
else if (key == SDLK_Q) {
809 rotatePieceColors(
false);
813 void handleNameKey(SDL_Keycode key) {
814 if (key == SDLK_ESCAPE) {
816 SDL_StopTextInput(
window.get());
821 if (key == SDLK_BACKSPACE) {
822 if (!player_name.empty()) {
823 player_name.pop_back();
828 if (key == SDLK_RETURN) {
833 void handleNameText(
const char *text) {
834 if (text ==
nullptr) {
838 while (*text !=
'\0' &&
static_cast<int>(player_name.size()) < max_name_length) {
839 const unsigned char ch =
static_cast<unsigned char>(*text++);
840 if (ch >= 32 && ch < 127) {
841 player_name.push_back(
static_cast<char>(ch));
848 if (player_name.empty()) {
849 player_name =
"Player";
851 high_scores.add(player_name, score);
855 SDL_StopTextInput(
window.get());
860 piece.colors = piece.next_colors;
861 piece.next_colors = randomColors();
865 if (!canPlacePiece(piece.x, piece.y)) {
870 bool boardHasFlashCells()
const {
871 for (
const auto &row : board) {
872 for (
const Cell &cell : row) {
873 if (cell.flash_until != 0U) {
881 bool updateFlashState(Uint64 now) {
882 if (!boardHasFlashCells()) {
886 bool expired_any =
false;
887 for (
auto &row : board) {
888 for (Cell &cell : row) {
889 if (cell.flash_until != 0U && now >= cell.flash_until) {
896 if (boardHasFlashCells()) {
905 if (!boardHasFlashCells() && waiting_for_spawn) {
906 waiting_for_spawn =
false;
913 void updateGame(Uint64 now) {
914 if (paused || awaiting_name) {
918 if (last_update_ms == 0U) {
919 last_update_ms = now;
923 if (updateFlashState(now)) {
924 last_update_ms = now;
928 if (waiting_for_spawn) {
929 waiting_for_spawn =
false;
931 last_update_ms = now;
935 const Uint64 delta = now - last_update_ms;
936 last_update_ms = now;
937 fall_accumulator_ms += delta;
939 while (fall_accumulator_ms >=
static_cast<Uint64
>(fall_delay_ms)) {
940 fall_accumulator_ms -=
static_cast<Uint64
>(fall_delay_ms);
941 if (!movePiece(0, 1)) {
948 bool resolveMatches(Uint64 now) {
949 std::array<std::array<bool, board_cols>,
board_rows> marked{};
950 int matches_found = 0;
952 auto mark_run = [&](
int x,
int y,
int dx,
int dy) {
953 const int color = board[
static_cast<std::size_t
>(y)][
static_cast<std::size_t
>(x)].color;
958 while (cx >= 0 && cy >= 0 && cx < board_cols && cy < board_rows &&
959 board[
static_cast<std::size_t
>(cy)][
static_cast<std::size_t
>(cx)].color == color &&
960 board[
static_cast<std::size_t
>(cy)][
static_cast<std::size_t
>(cx)].flash_until == 0U) {
973 for (
int i = 0; i < length; ++i) {
974 marked[
static_cast<std::size_t
>(cy)][
static_cast<std::size_t
>(cx)] =
true;
982 const Cell &cell = board[
static_cast<std::size_t
>(y)][
static_cast<std::size_t
>(x)];
983 if (cell.color == 0 || cell.flash_until != 0U) {
987 if (x == 0 || board[
static_cast<std::size_t
>(y)][
static_cast<std::size_t
>(x - 1)].color != cell.color) {
988 mark_run(x, y, 1, 0);
990 if (y == 0 || board[
static_cast<std::size_t
>(y - 1)][
static_cast<std::size_t
>(x)].color != cell.color) {
991 mark_run(x, y, 0, 1);
993 if (x == 0 || y == 0 ||
994 board[
static_cast<std::size_t
>(y - 1)][
static_cast<std::size_t
>(x - 1)].color != cell.color) {
995 mark_run(x, y, 1, 1);
997 if (x == board_cols - 1 || y == 0 ||
998 board[
static_cast<std::size_t
>(y - 1)][
static_cast<std::size_t
>(x + 1)].color != cell.color) {
999 mark_run(x, y, -1, 1);
1004 if (matches_found == 0) {
1010 if (marked[
static_cast<std::size_t
>(y)][
static_cast<std::size_t
>(x)]) {
1011 board[
static_cast<std::size_t
>(y)][
static_cast<std::size_t
>(x)].flash_until = now +
flash_time_ms;
1017 lines += matches_found;
1018 lines_toward_speedup += matches_found;
1019 while (lines_toward_speedup >= lines_per_speedup) {
1022 fall_delay_ms = std::max(140, 520 - speed_level * 40);
1028 void applyGravity() {
1031 for (
int y = board_rows - 1; y >= 0; --y) {
1032 if (board[
static_cast<std::size_t
>(y)][
static_cast<std::size_t
>(x)].color != 0) {
1033 if (write_row != y) {
1034 board[
static_cast<std::size_t
>(write_row)][
static_cast<std::size_t
>(x)] =
1035 board[
static_cast<std::size_t
>(y)][
static_cast<std::size_t
>(x)];
1036 board[
static_cast<std::size_t
>(y)][
static_cast<std::size_t
>(x)] = {};
1042 for (
int y = write_row; y >= 0; --y) {
1043 board[
static_cast<std::size_t
>(y)][
static_cast<std::size_t
>(x)] = {};
1048 void lockPiece(Uint64 now) {
1055 const int row = piece.y + i;
1056 board[
static_cast<std::size_t
>(row)][
static_cast<std::size_t
>(piece.x)].color =
1057 std::clamp(piece.colors[
static_cast<std::size_t
>(i)], 1, 9);
1060 if (resolveMatches(now)) {
1061 waiting_for_spawn =
true;
1068 void handleGameOver() {
1069 if (high_scores.qualifies(score)) {
1070 score_added =
false;
1071 player_name.clear();
1078 void drawSprite(mxvk::VK_Sprite *sprite,
int x,
int y,
int w,
int h) {
1079 if (sprite !=
nullptr) {
1084 void drawCenteredText(
const std::string &text,
int y,
const SDL_Color &color,
const mxvk::Font &font) {
1088 printText(text, scaled(32, layout.scale_x), y, color, font);
1092 const int x = std::max(16, (layout.width - w) / 2);
1096 bool drawIntro(Uint64 now) {
1097 const float elapsed = intro_start_ms == 0U ? 0.0f :
static_cast<float>(now - intro_start_ms) / 1000.0f;
1098 background_intro->setShaderParams(elapsed, 0.0f, 0.0f, 1.0f);
1099 drawSprite(background_intro, 0, 0, layout.width, layout.height);
1101 if (intro_start_ms == 0U) {
1102 intro_start_ms = now;
1105 if (now - intro_start_ms > title_screen_time_ms) {
1114 const float elapsed =
static_cast<float>(SDL_GetTicks()) / 1000.0f;
1115 background_menu->setShaderParams(elapsed, 0.0f, 0.0f, 1.0f);
1116 drawSprite(background_menu, 0, 0, layout.width, layout.height);
1119 const int y = layout.menu_y + i * layout.menu_step;
1120 if (i == menu_selection) {
1121 drawSprite(cursor, layout.menu_x - scaled(94, layout.scale_x), y + scaled(12, layout.scale_y), scaled(78, layout.scale_x), scaled(58, layout.scale_y));
1123 drawSprite(menu_items[
static_cast<std::size_t
>(i)], layout.menu_x, y, layout.menu_w, layout.menu_h);
1127 void drawScores(
bool entering_name) {
1128 const float elapsed =
static_cast<float>(SDL_GetTicks()) / 1000.0f;
1129 background_menu->setShaderParams(elapsed, 0.0f, 0.0f, 1.0f);
1130 drawSprite(background_menu, 0, 0, layout.width, layout.height);
1131 drawSprite(overlay, scaled(30, layout.scale_x), scaled(70, layout.scale_y),
1132 layout.width - scaled(60, layout.scale_x), layout.height - scaled(120, layout.scale_y));
1134 drawCenteredText(
"High Scores", scaled(72, layout.scale_y), SDL_Color{255, 245, 200, 255}, title_font);
1136 const auto &entries = high_scores.list();
1137 const int start_y = scaled(140, layout.scale_y);
1138 const int step_y = scaled(30, layout.scale_y);
1139 for (std::size_t i = 0; i < entries.size(); ++i) {
1140 const std::string line = std::format(
"{:>2}. {:<16} {}", i + 1, entries[i].name, entries[i].score);
1141 printText(line, scaled(70, layout.scale_x), start_y +
static_cast<int>(i) * step_y, SDL_Color{255, 255, 255, 255}, ui_font);
1144 if (entering_name) {
1145 printText(
"Type your name and press Enter", scaled(70, layout.scale_x), scaled(450, layout.scale_y), SDL_Color{240, 220, 220, 255}, ui_font);
1146 printText(
"Name:", scaled(70, layout.scale_x), scaled(490, layout.scale_y), SDL_Color{255, 245, 200, 255}, ui_font);
1147 printText(player_name +
"_", scaled(150, layout.scale_x), scaled(490, layout.scale_y), SDL_Color{255, 255, 255, 255}, ui_font);
1149 printText(
"Press Enter to return to the menu", scaled(70, layout.scale_x), scaled(490, layout.scale_y), SDL_Color{240, 240, 220, 255}, ui_font);
1153 void drawCredits() {
1154 const float elapsed =
static_cast<float>(SDL_GetTicks()) / 1000.0f;
1155 background_menu->setShaderParams(elapsed, 0.0f, 0.0f, 1.0f);
1156 drawSprite(background_menu, 0, 0, layout.width, layout.height);
1157 drawSprite(overlay, scaled(30, layout.scale_x), scaled(85, layout.scale_y),
1158 layout.width - scaled(60, layout.scale_x), scaled(260, layout.scale_y));
1159 const int logo_w = layout.width / 2;
1160 const int logo_h =
static_cast<int>(std::lround(
1161 static_cast<float>(logo_w) *
static_cast<float>(mxvk_logo->getHeight()) /
1162 static_cast<float>(mxvk_logo->getWidth())));
1163 const int logo_x = (layout.width - logo_w) / 2;
1164 const int logo_y = (layout.height - logo_h) / 2;
1165 drawSprite(mxvk_logo, logo_x, logo_y, logo_w, logo_h);
1166 drawCenteredText(
"Credits", scaled(96, layout.scale_y), SDL_Color{255, 245, 200, 255}, title_font);
1167 printText(
"Original game: MasterPiece", scaled(70, layout.scale_x), scaled(180, layout.scale_y), SDL_Color{255, 255, 255, 255}, ui_font);
1168 printText(
"MXVK port and cleanup: Vulkan example", scaled(70, layout.scale_x), scaled(214, layout.scale_y), SDL_Color{255, 255, 255, 255}, ui_font);
1169 printText(
"Press Enter or Escape to return", scaled(70, layout.scale_x), scaled(270, layout.scale_y), SDL_Color{240, 240, 220, 255}, ui_font);
1172 void drawGame(Uint64 now) {
1173 const float scaleX =
static_cast<float>(layout.width) /
static_cast<float>(game_base_width);
1174 const float scaleY =
static_cast<float>(layout.height) /
static_cast<float>(game_base_height);
1175 drawSprite(background_game, 0, 0, layout.width, layout.height);
1177 drawHud(scaleX, scaleY);
1180 const char *pausedText =
"PAUSED - Press P to Continue";
1181 int pausedWidth = 0;
1182 int pausedHeight = 0;
1184 pausedWidth =
static_cast<int>(std::strlen(pausedText)) * 16;
1186 printText(pausedText, layout.width / 2 - pausedWidth / 2, layout.height / 2, SDL_Color{255, 255, 0, 255}, title_font);
1190 void drawCubeScene(Uint64 now) {
1191 ensureFramebuffer();
1192 if (frame_sprite ==
nullptr || frame_surface ==
nullptr || frame_format ==
nullptr) {
1196 const float software_scale_x =
static_cast<float>(frame_width) /
static_cast<float>(game_base_width);
1197 const float software_scale_y =
static_cast<float>(frame_height) /
static_cast<float>(game_base_height);
1198 clearFrame(transparent_black);
1199 drawBoard(now, software_scale_x, software_scale_y);
1200 drawNextPiece(now, software_scale_x, software_scale_y);
1201 frame_sprite->updateTexture(frame_surface.get());
1202 frame_sprite->drawSpriteRect(0, 0, layout.width, layout.height);
1205 void updateGridViewInput(Uint64 now) {
1206 if (last_view_update_ms == 0U) {
1207 last_view_update_ms = now;
1211 const float delta_seconds =
static_cast<float>(now - last_view_update_ms) * 0.001f;
1212 last_view_update_ms = now;
1217 const bool *keys = SDL_GetKeyboardState(
nullptr);
1218 if (keys ==
nullptr) {
1222 if (keys[SDL_SCANCODE_A]) {
1225 if (keys[SDL_SCANCODE_D]) {
1228 if (keys[SDL_SCANCODE_W]) {
1229 grid_pitch = std::clamp(grid_pitch + GRID_PITCH_SPEED * delta_seconds, GRID_MIN_PITCH, GRID_MAX_PITCH);
1231 if (keys[SDL_SCANCODE_S]) {
1232 grid_pitch = std::clamp(grid_pitch - GRID_PITCH_SPEED * delta_seconds, GRID_MIN_PITCH, GRID_MAX_PITCH);
1234 if (keys[SDL_SCANCODE_PAGEUP]) {
1235 camera_distance = std::max(GRID_MIN_CAMERA_DISTANCE, camera_distance - GRID_ZOOM_SPEED * delta_seconds);
1237 if (keys[SDL_SCANCODE_PAGEDOWN]) {
1238 camera_distance = std::min(GRID_MAX_CAMERA_DISTANCE, camera_distance + GRID_ZOOM_SPEED * delta_seconds);
1242 [[nodiscard]] GridCubePlacement transformGridCube(
int grid_x,
int grid_y,
int size,
float scaleX,
float scaleY)
const {
1252 const float pitch = grid_pitch * 3.14159265358979323846f / 180.0f;
1253 const float yaw = grid_yaw * 3.14159265358979323846f / 180.0f;
1254 const float cos_pitch = std::cos(pitch);
1255 const float sin_pitch = std::sin(pitch);
1256 const float cos_yaw = std::cos(yaw);
1257 const float sin_yaw = std::sin(yaw);
1258 const float local_x = (
static_cast<float>(grid_x) - (
static_cast<float>(
board_cols) - 1.0f) * 0.5f) * cell_step_x;
1259 const float local_y = (
static_cast<float>(grid_y) - (
static_cast<float>(
board_rows) - 1.0f) * 0.5f) * cell_step_y;
1260 const float pitched_y = local_y * cos_pitch;
1261 const float pitched_z = local_y * sin_pitch;
1262 const float yawed_x = local_x * cos_yaw + pitched_z * sin_yaw;
1263 const float yawed_z = -local_x * sin_yaw + pitched_z * cos_yaw;
1264 const float focal_length = std::max(
static_cast<float>(frame_width),
static_cast<float>(frame_height)) * 1.1f;
1266 const float perspective_scale = std::clamp(focal_length / std::max(1.0f, camera_z + yawed_z), 0.35f, 2.3f);
1268 static_cast<int>(std::lround(board_center_x + yawed_x * perspective_scale)),
1269 static_cast<int>(std::lround(board_center_y + pitched_y * perspective_scale)),
1270 std::max(4,
static_cast<int>(std::lround(
static_cast<float>(size) * perspective_scale))),
1274 void drawBoard(Uint64 now,
float scaleX,
float scaleY) {
1275 const int cube_size = std::max(10,
static_cast<int>(std::lround(
static_cast<float>(game_block_height + game_block_spacing) * scaleY * 0.72f)));
1279 const Cell &cell = board[
static_cast<std::size_t
>(j)][
static_cast<std::size_t
>(i)];
1280 if (cell.color == 0) {
1284 const int sprite_index = cell.flash_until != 0U ? flashingSpriteIndex(i, j, now) : std::clamp(cell.color, 0, 9);
1285 const GridCubePlacement cube = transformGridCube(i, j, cube_size, scaleX, scaleY);
1291 static_cast<float>(i * 23 + j * 11),
1292 cell.flash_until != 0U,
1302 const int row = piece.y + i;
1303 if (row < 0 || row >= board_rows || piece.x < 0 || piece.x >= board_cols) {
1307 const GridCubePlacement cube = transformGridCube(piece.x, row, cube_size, scaleX, scaleY);
1312 std::clamp(piece.colors[
static_cast<std::size_t
>(i)], 0, 9),
1313 static_cast<float>(piece.x * 23 + row * 11),
1319 void drawNextPiece(Uint64 now,
float scaleX,
float scaleY) {
1323 const int cube_size = std::max(10,
static_cast<int>(std::lround(
static_cast<float>(game_block_height + game_block_spacing) * scaleY * 0.72f)));
1326 const int sprite_index = std::clamp(piece.next_colors[
static_cast<std::size_t
>(i)], 0, 9);
1328 static_cast<int>(std::lround((
static_cast<float>(bx) +
static_cast<float>(game_block_width) * 0.5f) * scaleX)),
1329 static_cast<int>(std::lround((
static_cast<float>(by + i * row_step) +
static_cast<float>(game_block_height) * 0.5f) * scaleY)),
1332 static_cast<float>(i * 31) +
static_cast<float>(now % 1000U) * 0.01f,
1338 void ensureFramebuffer() {
1339 if (frame_surface !=
nullptr) {
1344 frame_format = SDL_GetPixelFormatDetails(frame_surface->format);
1345 if (frame_format ==
nullptr) {
1346 throw mxvk::Exception(std::format(
"Failed to query 3dmath_masterpiece frame format: {}", SDL_GetError()));
1349 clearFrame(transparent_black);
1351 frame_sprite->setTextureFilter(VK_FILTER_NEAREST);
1354 [[nodiscard]] std::uint32_t mapColor(
mxvk::MXCOLOR color)
const {
1359 SDL_FillSurfaceRect(frame_surface.get(),
nullptr, mapColor(color));
1363 if (x < 0 || y < 0 || x >= frame_width || y >= frame_height) {
1367 auto *row =
static_cast<std::uint8_t *
>(frame_surface->pixels) + (
static_cast<std::size_t
>(y) *
static_cast<std::size_t
>(frame_surface->pitch));
1368 auto *pixel =
reinterpret_cast<std::uint32_t *
>(row) + x;
1369 *pixel = mapColor(color);
1373 static constexpr std::array<mxvk::MXCOLOR, 10> colors{{
1385 return colors[
static_cast<std::size_t
>(std::clamp(color, 0, 9))];
1388 [[nodiscard]]
static mxvk::vec4D projectCubePoint(
const mxvk::vec4D &point,
const CubeInstance &cube) {
1389 const float scale =
static_cast<float>(cube.size) * 2.25f;
1390 const float z = std::max(point.
z, 0.001f);
1392 static_cast<float>(cube.center_x) + (point.
x / z) * scale,
1393 static_cast<float>(cube.center_y) - (point.
y / z) * scale,
1399 void drawCube(
const CubeInstance &cube) {
1400 const float time =
static_cast<float>(SDL_GetTicks()) * 0.001f;
1401 const float flash_scale = cube.flashing ? 0.72f + std::sin(time * 24.0f) * 0.28f : 1.0f;
1403 const std::array<mxvk::vec4D, 8> cube_vertices = {
1404 mxvk::vec4D{-1.0f, -1.0f, -1.0f, 1.0f},
1405 mxvk::vec4D{1.0f, -1.0f, -1.0f, 1.0f},
1406 mxvk::vec4D{1.0f, 1.0f, -1.0f, 1.0f},
1407 mxvk::vec4D{-1.0f, 1.0f, -1.0f, 1.0f},
1408 mxvk::vec4D{-1.0f, -1.0f, 1.0f, 1.0f},
1409 mxvk::vec4D{1.0f, -1.0f, 1.0f, 1.0f},
1410 mxvk::vec4D{1.0f, 1.0f, 1.0f, 1.0f},
1411 mxvk::vec4D{-1.0f, 1.0f, 1.0f, 1.0f},
1413 const std::array<std::array<int, 4>, 6> cube_faces = {{
1422 mxvk::Mat4D rotation;
1423 rotation.
BuildXYZ(0.0f, time * 112.0f + cube.phase, 0.0f);
1425 std::array<mxvk::vec4D, 8> camera_vertices{};
1426 std::array<mxvk::vec4D, 8> projected{};
1427 for (std::size_t i = 0; i < cube_vertices.size(); ++i) {
1428 mxvk::vec4D point = rotation.
MulVec(cube_vertices[i]);
1430 camera_vertices[i] = point;
1431 projected[i] = projectCubePoint(point, cube);
1434 mxvk::vec3D light_dir(-0.35f, -0.55f, -1.0f);
1435 light_dir.Normalize();
1437 std::vector<FaceDraw> faces;
1438 faces.reserve(cube_faces.size());
1439 for (
const auto &indices : cube_faces) {
1440 const mxvk::vec4D &a = camera_vertices[
static_cast<std::size_t
>(indices[0])];
1441 const mxvk::vec4D &b = camera_vertices[
static_cast<std::size_t
>(indices[1])];
1442 const mxvk::vec4D &c = camera_vertices[
static_cast<std::size_t
>(indices[2])];
1443 mxvk::vec4D normal = mxvk::vec4D().Build(a, b).CrossProduct(mxvk::vec4D().Build(a, c));
1446 const mxvk::vec4D center = (a + b + c + camera_vertices[
static_cast<std::size_t
>(indices[3])]) * 0.25f;
1447 const mxvk::vec4D view_vector(-center.
x, -center.
y, -center.
z, 1.0f);
1448 if (normal.DotProduct(view_vector) <= 0.0f) {
1452 const float diffuse = std::max(0.0f, normal.DotProduct(mxvk::vec4D(light_dir.x, light_dir.y, light_dir.z, 1.0f)));
1453 const float intensity = std::clamp(0.28f + diffuse * 0.72f, 0.0f, 1.0f);
1457 std::ranges::sort(faces, [](
const FaceDraw &left,
const FaceDraw &right) {
1458 return left.depth > right.depth;
1461 for (
const FaceDraw &face : faces) {
1462 mxvk::PipeLine fill_pipeline;
1463 fill_pipeline.
Begin(frame_width, frame_height, [
this](
int x,
int y,
mxvk::MXCOLOR color) { putPixel(x, y, color); });
1464 const mxvk::vec4D &a = projected[
static_cast<std::size_t
>(face.indices[0])];
1465 const mxvk::vec4D &b = projected[
static_cast<std::size_t
>(face.indices[1])];
1466 const mxvk::vec4D &c = projected[
static_cast<std::size_t
>(face.indices[2])];
1467 const mxvk::vec4D &d = projected[
static_cast<std::size_t
>(face.indices[3])];
1470 fill_pipeline.
End();
1474 void drawHud(
float scaleX,
float scaleY) {
1475 printText(std::format(
"Score: {}", score),
1476 static_cast<int>(200.0f * scaleX),
1477 static_cast<int>(80.0f * scaleY) - 24,
1478 SDL_Color{255, 255, 255, 255},
1480 printText(std::format(
"Tabs: {}", lines),
1481 static_cast<int>(310.0f * scaleX),
1482 static_cast<int>(80.0f * scaleY) - 24,
1483 SDL_Color{255, 255, 255, 255},
1487 void handleControllerAxis(Sint16 lx, Sint16 ly) {
1488 const Uint32 now = SDL_GetTicks();
1491 if (ly < -joystick_dead_zone) {
1492 if (now - joy_repeat_up_ms > joy_repeat_delay_ms) {
1493 menu_selection = (menu_selection +
menu_item_count - 1) % menu_item_count;
1494 joy_repeat_up_ms = now;
1497 joy_repeat_up_ms = 0U;
1500 if (ly > joystick_dead_zone) {
1501 if (now - joy_repeat_down_ms > joy_repeat_delay_ms) {
1502 menu_selection = (menu_selection + 1) % menu_item_count;
1503 joy_repeat_down_ms = now;
1506 joy_repeat_down_ms = 0U;
1511 if (screen !=
Screen::Game || paused || awaiting_name) {
1515 if (lx < -joystick_dead_zone) {
1516 if (now - joy_repeat_left_ms > joy_repeat_delay_ms) {
1518 joy_repeat_left_ms = now;
1521 joy_repeat_left_ms = 0U;
1524 if (lx > joystick_dead_zone) {
1525 if (now - joy_repeat_right_ms > joy_repeat_delay_ms) {
1527 joy_repeat_right_ms = now;
1530 joy_repeat_right_ms = 0U;
1533 if (ly > joystick_dead_zone) {
1534 if (now - joy_repeat_down_ms > joy_repeat_delay_ms) {
1535 if (!movePiece(0, 1)) {
1538 joy_repeat_down_ms = now;
1541 joy_repeat_down_ms = 0U;
1544 if (lx == 0 && ly == 0) {
1545 joy_repeat_up_ms = 0U;
1549 void pollController([[maybe_unused]] Uint64 now) {
1550 if (gamepad ==
nullptr) {
1554 const Sint16 lx = SDL_GetGamepadAxis(gamepad, SDL_GAMEPAD_AXIS_LEFTX);
1555 const Sint16 ly = SDL_GetGamepadAxis(gamepad, SDL_GAMEPAD_AXIS_LEFTY);
1556 handleControllerAxis(lx, ly);
1559 bool openGamepad(SDL_JoystickID
id) {
1560 if (gamepad !=
nullptr && gamepadId ==
id) {
1565 gamepad = SDL_OpenGamepad(
id);
1566 if (gamepad ==
nullptr) {
1574 void tryOpenFirstGamepad() {
1575 if (gamepad !=
nullptr) {
1580 SDL_JoystickID *ids = SDL_GetGamepads(&count);
1581 if (ids ==
nullptr || count <= 0) {
1582 if (ids !=
nullptr) {
1588 openGamepad(ids[0]);
1592 void closeGamepad() {
1593 if (gamepad !=
nullptr) {
1594 SDL_CloseGamepad(gamepad);