291 MasterPieceWindow(
const std::string &path,
int width,
int height,
bool fullscreen,
bool enable_vsync,
const FramebufferDimensions &framebuffer) :
mxvk::
VK_Window(
"3D Math MasterPiece", width, height, fullscreen,
MXVK_VALIDATION, enable_vsync), asset_root(resolveAssetRoot(path)), puzzle_asset_root(resolvePuzzleAssetRoot(resolveAssetRoot(path))), high_scores(scorePath(asset_root)), frame_width(framebuffer.width), frame_height(framebuffer.height) {
295 const std::string
font_path = dataPath(
"font.ttf");
303 tryOpenFirstGamepad();
309 if (e.type == SDL_EVENT_QUIT) {
314 if (e.type == SDL_EVENT_GAMEPAD_ADDED) {
315 openGamepad(e.gdevice.which);
319 if (e.type == SDL_EVENT_GAMEPAD_REMOVED) {
320 if (gamepad !=
nullptr && e.gdevice.which == gamepadId) {
322 tryOpenFirstGamepad();
327 if (e.type == SDL_EVENT_GAMEPAD_BUTTON_DOWN) {
328 handleControllerButton(e.gbutton.button);
333 handleNameText(e.text.text);
337 if (e.type != SDL_EVENT_KEY_DOWN) {
343 if (isConfirmKey(e.key.key) || e.key.key == SDLK_ESCAPE) {
348 handleMenuKey(e.key.key);
351 handleGameKey(e.key.key);
355 if (e.key.key == SDLK_RETURN || e.key.key == SDLK_ESCAPE) {
360 handleNameKey(e.key.key);
366 const Uint64 now = SDL_GetTicks();
367 layout = computeLayout();
368 updateGridViewInput(now);
373 if (!drawIntro(now)) {
408 static constexpr std::array<const char *, menu_item_count> menu_files{{
410 "menu_high_scores.png",
415 std::filesystem::path asset_root;
416 std::filesystem::path puzzle_asset_root;
420 std::mt19937 rng{std::random_device{}()};
421 std::array<std::array<Cell, board_cols>,
board_rows> board{};
423 mxvk::Font title_font{};
424 mxvk::Font ui_font{};
425 mxvk::VK_Sprite *background_intro =
nullptr;
426 mxvk::VK_Sprite *mxvk_logo =
nullptr;
427 mxvk::VK_Sprite *background_menu =
nullptr;
428 mxvk::VK_Sprite *background_game =
nullptr;
429 mxvk::VK_Sprite *cursor =
nullptr;
430 std::array<mxvk::VK_Sprite *, menu_files.size()> menu_items{};
431 mxvk::VK_Sprite *panel =
nullptr;
432 mxvk::VK_Sprite *overlay =
nullptr;
434 const SDL_PixelFormatDetails *frame_format =
nullptr;
435 mxvk::VK_Sprite *frame_sprite =
nullptr;
436 int frame_width = 1280;
437 int frame_height = 720;
438 std::string player_name;
439 int menu_selection = 0;
443 int lines_toward_speedup = 0;
444 int fall_delay_ms = 520;
445 Uint64 intro_start_ms = 0;
446 Uint64 last_update_ms = 0;
447 Uint64 fall_accumulator_ms = 0;
448 Uint64 last_view_update_ms = 0;
449 float grid_pitch = 0.0f;
450 float grid_yaw = 0.0f;
452 Uint32 joy_repeat_left_ms = 0;
453 Uint32 joy_repeat_right_ms = 0;
454 Uint32 joy_repeat_up_ms = 0;
455 Uint32 joy_repeat_down_ms = 0;
456 SDL_Gamepad *gamepad =
nullptr;
457 SDL_JoystickID gamepadId = 0;
459 bool awaiting_name =
false;
460 bool score_added =
false;
461 bool waiting_for_spawn =
false;
463 static bool isConfirmKey(SDL_Keycode key) {
return key == SDLK_RETURN || key == SDLK_SPACE; }
465 static int scaled(
int value,
float scale) {
return std::max(1,
static_cast<int>(std::lround(
static_cast<float>(value) * scale))); }
467 static int scaledPos(
int value,
float scale) {
return static_cast<int>(std::lround(
static_cast<float>(value) * scale)); }
469 static int flashingSpriteIndex(
int x,
int y, Uint64 now) {
470 const Uint64 tick = now / 18U;
471 const Uint64 mixed = tick +
static_cast<Uint64
>(x * 37 + y * 101);
472 return static_cast<int>((mixed % 9U) + 1U);
475 [[nodiscard]] Layout computeLayout()
const {
478 result.width = extent.width == 0U ?
base_width :
static_cast<int>(extent.width);
479 result.height = extent.height == 0U ?
base_height :
static_cast<int>(extent.height);
480 result.scale_x =
static_cast<float>(result.width) /
static_cast<float>(base_width);
481 result.scale_y =
static_cast<float>(result.height) /
static_cast<float>(base_height);
482 result.game_scale = std::min(result.scale_x, result.scale_y);
483 result.game_w = scaled(base_width, result.game_scale);
484 result.game_h = scaled(base_height, result.game_scale);
485 result.game_x = (result.width - result.game_w) / 2;
486 result.game_y = (result.height - result.game_h) / 2;
487 result.board_x = result.game_x + scaledPos(185, result.game_scale);
488 result.board_y = result.game_y + scaledPos(95, result.game_scale);
489 result.cell_w = scaled(32, result.game_scale);
490 result.cell_h = scaled(16, result.game_scale);
491 result.next_x = result.game_x + scaledPos(510, result.game_scale);
492 result.next_y = result.game_y + scaledPos(200, result.game_scale);
493 result.menu_x = scaled(505, result.scale_x);
494 result.menu_y = scaled(400, result.scale_y);
495 result.menu_w = scaled(430, result.scale_x);
496 result.menu_h = scaled(82, result.scale_y);
497 result.menu_step = scaled(118, result.scale_y);
501 std::string dataPath(
const char *name)
const {
return (asset_root /
"data" / name).string(); }
503 std::string puzzleDataPath(
const char *name)
const {
504 const std::filesystem::path shared_path = puzzle_asset_root /
"data" / name;
505 if (std::filesystem::exists(shared_path)) {
506 return shared_path.string();
508 return dataPath(name);
511 mxvk::VK_Sprite *loadPngSprite(
const char *name) {
return createSprite(dataPath(name)); }
513 mxvk::VK_Sprite *loadEffectSprite(
const char *name) {
return createSprite(dataPath(name),
"", dataPath(
"intro.frag.spv")); }
515 mxvk::VK_Sprite *makeSolidPixel(std::uint8_t r, std::uint8_t g, std::uint8_t b, std::uint8_t a) {
516 const std::array<std::uint8_t, 4> pixel{r, g, b, a};
523 background_intro = loadEffectSprite(
"intro.png");
524 background_menu = loadEffectSprite(
"start.png");
526 mxvk_logo =
createSprite(puzzleDataPath(
"mxvk_logo.png"));
527 cursor = loadPngSprite(
"cursor.png");
529 for (std::size_t i = 0; i < menu_files.size(); ++i) {
530 menu_items[i] = loadPngSprite(menu_files[i]);
533 panel = makeSolidPixel(0, 0, 0, 192);
534 overlay = makeSolidPixel(0, 0, 0, 128);
537 void setScreen(Screen next) {
539 SDL_StopTextInput(
window.get());
547 SDL_StartTextInput(
window.get());
552 intro_start_ms = SDL_GetTicks();
557 for (
auto &row : board) {
558 for (Cell &cell : row) {
566 lines_toward_speedup = 0;
569 fall_accumulator_ms = 0;
571 awaiting_name =
false;
573 waiting_for_spawn =
false;
576 piece.next_colors = randomColors();
580 std::array<int, piece_height> randomColors() {
581 std::uniform_int_distribution<int> dist(1, 9);
582 std::array<int, piece_height> colors{dist(rng), dist(rng), dist(rng)};
583 while (colors[0] == colors[1] && colors[1] == colors[2]) {
584 colors[1] = dist(rng);
589 bool canPlacePiece(
int x,
int y)
const {
590 if (x < 0 || x >= board_cols) {
595 const int row = y + i;
596 if (row < 0 || row >= board_rows) {
599 if (board[
static_cast<std::size_t
>(row)][
static_cast<std::size_t
>(x)].color != 0) {
606 bool movePiece(
int dx,
int dy) {
607 const int next_x = piece.x + dx;
608 const int next_y = piece.y + dy;
609 if (!canPlacePiece(next_x, next_y)) {
619 while (movePiece(0, 1)) {
621 lockPiece(SDL_GetTicks());
624 void rotatePieceColors(
bool forward) {
626 const int temp = piece.colors.back();
627 piece.colors[2] = piece.colors[1];
628 piece.colors[1] = piece.colors[0];
629 piece.colors[0] = temp;
631 const int temp = piece.colors.front();
632 piece.colors[0] = piece.colors[1];
633 piece.colors[1] = piece.colors[2];
634 piece.colors[2] = temp;
638 void handleControllerButton(Uint8 button) {
641 if (button == SDL_GAMEPAD_BUTTON_SOUTH || button == SDL_GAMEPAD_BUTTON_START) {
646 if (button == SDL_GAMEPAD_BUTTON_DPAD_UP) {
647 menu_selection = (menu_selection +
menu_item_count - 1) % menu_item_count;
648 }
else if (button == SDL_GAMEPAD_BUTTON_DPAD_DOWN) {
649 menu_selection = (menu_selection + 1) % menu_item_count;
650 }
else if (button == SDL_GAMEPAD_BUTTON_SOUTH || button == SDL_GAMEPAD_BUTTON_START) {
651 handleMenuSelection();
652 }
else if (button == SDL_GAMEPAD_BUTTON_EAST || button == SDL_GAMEPAD_BUTTON_BACK) {
657 if (button == SDL_GAMEPAD_BUTTON_BACK) {
659 }
else if (button == SDL_GAMEPAD_BUTTON_START) {
661 }
else if (paused || awaiting_name) {
663 }
else if (button == SDL_GAMEPAD_BUTTON_DPAD_LEFT) {
665 }
else if (button == SDL_GAMEPAD_BUTTON_DPAD_RIGHT) {
667 }
else if (button == SDL_GAMEPAD_BUTTON_DPAD_DOWN) {
668 if (!movePiece(0, 1)) {
669 lockPiece(SDL_GetTicks());
671 }
else if (button == SDL_GAMEPAD_BUTTON_DPAD_UP || button == SDL_GAMEPAD_BUTTON_SOUTH) {
672 rotatePieceColors(
true);
673 }
else if (button == SDL_GAMEPAD_BUTTON_EAST) {
674 rotatePieceColors(
false);
675 }
else if (button == SDL_GAMEPAD_BUTTON_NORTH) {
681 if (button == SDL_GAMEPAD_BUTTON_SOUTH || button == SDL_GAMEPAD_BUTTON_START) {
683 }
else if (button == SDL_GAMEPAD_BUTTON_EAST || button == SDL_GAMEPAD_BUTTON_BACK) {
685 SDL_StopTextInput(
window.get());
688 }
else if (button == SDL_GAMEPAD_BUTTON_SOUTH || button == SDL_GAMEPAD_BUTTON_START || button == SDL_GAMEPAD_BUTTON_EAST || button == SDL_GAMEPAD_BUTTON_BACK) {
693 if (button == SDL_GAMEPAD_BUTTON_SOUTH || button == SDL_GAMEPAD_BUTTON_START || button == SDL_GAMEPAD_BUTTON_EAST || button == SDL_GAMEPAD_BUTTON_BACK) {
698 if (button == SDL_GAMEPAD_BUTTON_SOUTH || button == SDL_GAMEPAD_BUTTON_START) {
700 }
else if (button == SDL_GAMEPAD_BUTTON_EAST || button == SDL_GAMEPAD_BUTTON_BACK) {
702 SDL_StopTextInput(
window.get());
709 void handleMenuSelection() {
710 switch (menu_selection) {
729 void handleMenuKey(SDL_Keycode key) {
730 if (key == SDLK_ESCAPE) {
735 if (key == SDLK_UP) {
736 menu_selection = (menu_selection +
menu_item_count - 1) % menu_item_count;
740 if (key == SDLK_DOWN) {
741 menu_selection = (menu_selection + 1) % menu_item_count;
745 if (!isConfirmKey(key)) {
749 handleMenuSelection();
752 void handleGameKey(SDL_Keycode key) {
753 if (key == SDLK_ESCAPE) {
758 if (key ==
'p' || key ==
'P') {
763 if (paused || awaiting_name) {
767 if (key == SDLK_LEFT) {
769 }
else if (key == SDLK_RIGHT) {
771 }
else if (key == SDLK_DOWN) {
772 if (!movePiece(0, 1)) {
773 lockPiece(SDL_GetTicks());
775 }
else if (key == SDLK_UP) {
776 rotatePieceColors(
true);
777 }
else if (key == SDLK_Q) {
778 rotatePieceColors(
false);
782 void handleNameKey(SDL_Keycode key) {
783 if (key == SDLK_ESCAPE) {
785 SDL_StopTextInput(
window.get());
790 if (key == SDLK_BACKSPACE) {
791 if (!player_name.empty()) {
792 player_name.pop_back();
797 if (key == SDLK_RETURN) {
802 void handleNameText(
const char *text) {
803 if (text ==
nullptr) {
807 while (*text !=
'\0' &&
static_cast<int>(player_name.size()) < max_name_length) {
808 const unsigned char ch =
static_cast<unsigned char>(*text++);
809 if (ch >= 32 && ch < 127) {
810 player_name.push_back(
static_cast<char>(ch));
817 if (player_name.empty()) {
818 player_name =
"Player";
820 high_scores.add(player_name, score);
824 SDL_StopTextInput(
window.get());
829 piece.colors = piece.next_colors;
830 piece.next_colors = randomColors();
834 if (!canPlacePiece(piece.x, piece.y)) {
839 bool boardHasFlashCells()
const {
840 for (
const auto &row : board) {
841 for (
const Cell &cell : row) {
842 if (cell.flash_until != 0U) {
850 bool updateFlashState(Uint64 now) {
851 if (!boardHasFlashCells()) {
855 bool expired_any =
false;
856 for (
auto &row : board) {
857 for (Cell &cell : row) {
858 if (cell.flash_until != 0U && now >= cell.flash_until) {
865 if (boardHasFlashCells()) {
874 if (!boardHasFlashCells() && waiting_for_spawn) {
875 waiting_for_spawn =
false;
882 void updateGame(Uint64 now) {
883 if (paused || awaiting_name) {
887 if (last_update_ms == 0U) {
888 last_update_ms = now;
892 if (updateFlashState(now)) {
893 last_update_ms = now;
897 if (waiting_for_spawn) {
898 waiting_for_spawn =
false;
900 last_update_ms = now;
904 const Uint64 delta = now - last_update_ms;
905 last_update_ms = now;
906 fall_accumulator_ms += delta;
908 while (fall_accumulator_ms >=
static_cast<Uint64
>(fall_delay_ms)) {
909 fall_accumulator_ms -=
static_cast<Uint64
>(fall_delay_ms);
910 if (!movePiece(0, 1)) {
917 bool resolveMatches(Uint64 now) {
918 std::array<std::array<bool, board_cols>,
board_rows> marked{};
919 int matches_found = 0;
921 auto mark_run = [&](
int x,
int y,
int dx,
int dy) {
922 const int color = board[
static_cast<std::size_t
>(y)][
static_cast<std::size_t
>(x)].color;
927 while (cx >= 0 && cy >= 0 && cx < board_cols && cy < board_rows && board[
static_cast<std::size_t
>(cy)][
static_cast<std::size_t
>(cx)].color == color && board[
static_cast<std::size_t
>(cy)][
static_cast<std::size_t
>(cx)].flash_until == 0U) {
940 for (
int i = 0; i < length; ++i) {
941 marked[
static_cast<std::size_t
>(cy)][
static_cast<std::size_t
>(cx)] =
true;
949 const Cell &cell = board[
static_cast<std::size_t
>(y)][
static_cast<std::size_t
>(x)];
950 if (cell.color == 0 || cell.flash_until != 0U) {
954 if (x == 0 || board[
static_cast<std::size_t
>(y)][
static_cast<std::size_t
>(x - 1)].color != cell.color) {
955 mark_run(x, y, 1, 0);
957 if (y == 0 || board[
static_cast<std::size_t
>(y - 1)][
static_cast<std::size_t
>(x)].color != cell.color) {
958 mark_run(x, y, 0, 1);
960 if (x == 0 || y == 0 || board[
static_cast<std::size_t
>(y - 1)][
static_cast<std::size_t
>(x - 1)].color != cell.color) {
961 mark_run(x, y, 1, 1);
963 if (x == board_cols - 1 || y == 0 || board[
static_cast<std::size_t
>(y - 1)][
static_cast<std::size_t
>(x + 1)].color != cell.color) {
964 mark_run(x, y, -1, 1);
969 if (matches_found == 0) {
975 if (marked[
static_cast<std::size_t
>(y)][
static_cast<std::size_t
>(x)]) {
976 board[
static_cast<std::size_t
>(y)][
static_cast<std::size_t
>(x)].flash_until = now +
flash_time_ms;
982 lines += matches_found;
983 lines_toward_speedup += matches_found;
984 while (lines_toward_speedup >= lines_per_speedup) {
987 fall_delay_ms = std::max(140, 520 - speed_level * 40);
993 void applyGravity() {
996 for (
int y = board_rows - 1; y >= 0; --y) {
997 if (board[
static_cast<std::size_t
>(y)][
static_cast<std::size_t
>(x)].color != 0) {
998 if (write_row != y) {
999 board[
static_cast<std::size_t
>(write_row)][
static_cast<std::size_t
>(x)] = board[
static_cast<std::size_t
>(y)][
static_cast<std::size_t
>(x)];
1000 board[
static_cast<std::size_t
>(y)][
static_cast<std::size_t
>(x)] = {};
1006 for (
int y = write_row; y >= 0; --y) {
1007 board[
static_cast<std::size_t
>(y)][
static_cast<std::size_t
>(x)] = {};
1012 void lockPiece(Uint64 now) {
1019 const int row = piece.y + i;
1020 board[
static_cast<std::size_t
>(row)][
static_cast<std::size_t
>(piece.x)].color = std::clamp(piece.colors[
static_cast<std::size_t
>(i)], 1, 9);
1023 if (resolveMatches(now)) {
1024 waiting_for_spawn =
true;
1031 void handleGameOver() {
1032 if (high_scores.qualifies(score)) {
1033 score_added =
false;
1034 player_name.clear();
1041 void drawSprite(mxvk::VK_Sprite *sprite,
int x,
int y,
int w,
int h) {
1042 if (sprite !=
nullptr) {
1047 void drawCenteredText(
const std::string &text,
int y,
const SDL_Color &color,
const mxvk::Font &font) {
1051 printText(text, scaled(32, layout.scale_x), y, color, font);
1055 const int x = std::max(16, (layout.width - w) / 2);
1059 bool drawIntro(Uint64 now) {
1060 const float elapsed = intro_start_ms == 0U ? 0.0f :
static_cast<float>(now - intro_start_ms) / 1000.0f;
1061 background_intro->setShaderParams(elapsed, 0.0f, 0.0f, 1.0f);
1062 drawSprite(background_intro, 0, 0, layout.width, layout.height);
1064 if (intro_start_ms == 0U) {
1065 intro_start_ms = now;
1068 if (now - intro_start_ms > title_screen_time_ms) {
1077 const float elapsed =
static_cast<float>(SDL_GetTicks()) / 1000.0f;
1078 background_menu->setShaderParams(elapsed, 0.0f, 0.0f, 1.0f);
1079 drawSprite(background_menu, 0, 0, layout.width, layout.height);
1082 const int y = layout.menu_y + i * layout.menu_step;
1083 if (i == menu_selection) {
1084 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));
1086 drawSprite(menu_items[
static_cast<std::size_t
>(i)], layout.menu_x, y, layout.menu_w, layout.menu_h);
1090 void drawScores(
bool entering_name) {
1091 const float elapsed =
static_cast<float>(SDL_GetTicks()) / 1000.0f;
1092 background_menu->setShaderParams(elapsed, 0.0f, 0.0f, 1.0f);
1093 drawSprite(background_menu, 0, 0, layout.width, layout.height);
1094 drawSprite(overlay, scaled(30, layout.scale_x), scaled(70, layout.scale_y), layout.width - scaled(60, layout.scale_x), layout.height - scaled(120, layout.scale_y));
1096 drawCenteredText(
"High Scores", scaled(72, layout.scale_y), SDL_Color{255, 245, 200, 255}, title_font);
1098 const auto &entries = high_scores.list();
1099 const int start_y = scaled(140, layout.scale_y);
1100 const int step_y = scaled(30, layout.scale_y);
1101 for (std::size_t i = 0; i < entries.size(); ++i) {
1102 const std::string line = std::format(
"{:>2}. {:<16} {}", i + 1, entries[i].name, entries[i].score);
1103 printText(line, scaled(70, layout.scale_x), start_y +
static_cast<int>(i) * step_y, SDL_Color{255, 255, 255, 255}, ui_font);
1106 if (entering_name) {
1107 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);
1108 printText(
"Name:", scaled(70, layout.scale_x), scaled(490, layout.scale_y), SDL_Color{255, 245, 200, 255}, ui_font);
1109 printText(player_name +
"_", scaled(150, layout.scale_x), scaled(490, layout.scale_y), SDL_Color{255, 255, 255, 255}, ui_font);
1111 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);
1115 void drawCredits() {
1116 const float elapsed =
static_cast<float>(SDL_GetTicks()) / 1000.0f;
1117 background_menu->setShaderParams(elapsed, 0.0f, 0.0f, 1.0f);
1118 drawSprite(background_menu, 0, 0, layout.width, layout.height);
1119 drawSprite(overlay, scaled(30, layout.scale_x), scaled(85, layout.scale_y), layout.width - scaled(60, layout.scale_x), scaled(260, layout.scale_y));
1120 const int logo_w = layout.width / 2;
1121 const int logo_h =
static_cast<int>(std::lround(
static_cast<float>(logo_w) *
static_cast<float>(mxvk_logo->getHeight()) /
static_cast<float>(mxvk_logo->getWidth())));
1122 const int logo_x = (layout.width - logo_w) / 2;
1123 const int logo_y = (layout.height - logo_h) / 2;
1124 drawSprite(mxvk_logo, logo_x, logo_y, logo_w, logo_h);
1125 drawCenteredText(
"Credits", scaled(96, layout.scale_y), SDL_Color{255, 245, 200, 255}, title_font);
1126 printText(
"Original game: MasterPiece", scaled(70, layout.scale_x), scaled(180, layout.scale_y), SDL_Color{255, 255, 255, 255}, ui_font);
1127 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);
1128 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);
1131 void drawGame(Uint64 now) {
1132 const float scaleX =
static_cast<float>(layout.width) /
static_cast<float>(game_base_width);
1133 const float scaleY =
static_cast<float>(layout.height) /
static_cast<float>(game_base_height);
1134 drawSprite(background_game, 0, 0, layout.width, layout.height);
1136 drawHud(scaleX, scaleY);
1139 const char *pausedText =
"PAUSED - Press P to Continue";
1140 int pausedWidth = 0;
1141 int pausedHeight = 0;
1143 pausedWidth =
static_cast<int>(std::strlen(pausedText)) * 16;
1145 printText(pausedText, layout.width / 2 - pausedWidth / 2, layout.height / 2, SDL_Color{255, 255, 0, 255}, title_font);
1149 void drawCubeScene(Uint64 now) {
1150 ensureFramebuffer();
1151 if (frame_sprite ==
nullptr || frame_surface ==
nullptr || frame_format ==
nullptr) {
1155 const float software_scale_x =
static_cast<float>(frame_width) /
static_cast<float>(game_base_width);
1156 const float software_scale_y =
static_cast<float>(frame_height) /
static_cast<float>(game_base_height);
1157 clearFrame(transparent_black);
1158 drawBoard(now, software_scale_x, software_scale_y);
1159 drawNextPiece(now, software_scale_x, software_scale_y);
1160 frame_sprite->updateTexture(frame_surface.get());
1161 frame_sprite->drawSpriteRect(0, 0, layout.width, layout.height);
1164 void updateGridViewInput(Uint64 now) {
1165 if (last_view_update_ms == 0U) {
1166 last_view_update_ms = now;
1170 const float delta_seconds =
static_cast<float>(now - last_view_update_ms) * 0.001f;
1171 last_view_update_ms = now;
1176 const bool *keys = SDL_GetKeyboardState(
nullptr);
1177 if (keys ==
nullptr) {
1181 if (keys[SDL_SCANCODE_A]) {
1184 if (keys[SDL_SCANCODE_D]) {
1187 if (keys[SDL_SCANCODE_W]) {
1188 grid_pitch = std::clamp(grid_pitch + GRID_PITCH_SPEED * delta_seconds, GRID_MIN_PITCH, GRID_MAX_PITCH);
1190 if (keys[SDL_SCANCODE_S]) {
1191 grid_pitch = std::clamp(grid_pitch - GRID_PITCH_SPEED * delta_seconds, GRID_MIN_PITCH, GRID_MAX_PITCH);
1193 if (keys[SDL_SCANCODE_PAGEUP]) {
1194 camera_distance = std::max(GRID_MIN_CAMERA_DISTANCE, camera_distance - GRID_ZOOM_SPEED * delta_seconds);
1196 if (keys[SDL_SCANCODE_PAGEDOWN]) {
1197 camera_distance = std::min(GRID_MAX_CAMERA_DISTANCE, camera_distance + GRID_ZOOM_SPEED * delta_seconds);
1201 [[nodiscard]] GridCubePlacement transformGridCube(
int grid_x,
int grid_y,
int size,
float scaleX,
float scaleY)
const {
1206 const float pitch = grid_pitch * 3.14159265358979323846f / 180.0f;
1207 const float yaw = grid_yaw * 3.14159265358979323846f / 180.0f;
1208 const float cos_pitch = std::cos(pitch);
1209 const float sin_pitch = std::sin(pitch);
1210 const float cos_yaw = std::cos(yaw);
1211 const float sin_yaw = std::sin(yaw);
1212 const float local_x = (
static_cast<float>(grid_x) - (
static_cast<float>(
board_cols) - 1.0f) * 0.5f) * cell_step_x;
1213 const float local_y = (
static_cast<float>(grid_y) - (
static_cast<float>(
board_rows) - 1.0f) * 0.5f) * cell_step_y;
1214 const float pitched_y = local_y * cos_pitch;
1215 const float pitched_z = local_y * sin_pitch;
1216 const float yawed_x = local_x * cos_yaw + pitched_z * sin_yaw;
1217 const float yawed_z = -local_x * sin_yaw + pitched_z * cos_yaw;
1218 const float focal_length = std::max(
static_cast<float>(frame_width),
static_cast<float>(frame_height)) * 1.1f;
1220 const float perspective_scale = std::clamp(focal_length / std::max(1.0f, camera_z + yawed_z), 0.35f, 2.3f);
1222 static_cast<int>(std::lround(board_center_x + yawed_x * perspective_scale)),
1223 static_cast<int>(std::lround(board_center_y + pitched_y * perspective_scale)),
1224 std::max(4,
static_cast<int>(std::lround(
static_cast<float>(size) * perspective_scale))),
1228 void drawBoard(Uint64 now,
float scaleX,
float scaleY) {
1229 const int cube_size = std::max(10,
static_cast<int>(std::lround(
static_cast<float>(game_block_height + game_block_spacing) * scaleY * 0.72f)));
1233 const Cell &cell = board[
static_cast<std::size_t
>(j)][
static_cast<std::size_t
>(i)];
1234 if (cell.color == 0) {
1238 const int sprite_index = cell.flash_until != 0U ? flashingSpriteIndex(i, j, now) : std::clamp(cell.color, 0, 9);
1239 const GridCubePlacement cube = transformGridCube(i, j, cube_size, scaleX, scaleY);
1245 static_cast<float>(i * 23 + j * 11),
1246 cell.flash_until != 0U,
1256 const int row = piece.y + i;
1257 if (row < 0 || row >= board_rows || piece.x < 0 || piece.x >= board_cols) {
1261 const GridCubePlacement cube = transformGridCube(piece.x, row, cube_size, scaleX, scaleY);
1266 std::clamp(piece.colors[
static_cast<std::size_t
>(i)], 0, 9),
1267 static_cast<float>(piece.x * 23 + row * 11),
1273 void drawNextPiece(Uint64 now,
float scaleX,
float scaleY) {
1277 const int cube_size = std::max(10,
static_cast<int>(std::lround(
static_cast<float>(game_block_height + game_block_spacing) * scaleY * 0.72f)));
1280 const int sprite_index = std::clamp(piece.next_colors[
static_cast<std::size_t
>(i)], 0, 9);
1282 static_cast<int>(std::lround((
static_cast<float>(bx) +
static_cast<float>(game_block_width) * 0.5f) * scaleX)),
1283 static_cast<int>(std::lround((
static_cast<float>(by + i * row_step) +
static_cast<float>(game_block_height) * 0.5f) * scaleY)),
1286 static_cast<float>(i * 31) +
static_cast<float>(now % 1000U) * 0.01f,
1292 void ensureFramebuffer() {
1293 if (frame_surface !=
nullptr) {
1298 frame_format = SDL_GetPixelFormatDetails(frame_surface->format);
1299 if (frame_format ==
nullptr) {
1300 throw mxvk::Exception(std::format(
"Failed to query 3dmath_masterpiece frame format: {}", SDL_GetError()));
1303 clearFrame(transparent_black);
1305 frame_sprite->setTextureFilter(VK_FILTER_NEAREST);
1310 void clearFrame(
mxvk::MXCOLOR color) { SDL_FillSurfaceRect(frame_surface.get(),
nullptr, mapColor(color)); }
1313 if (x < 0 || y < 0 || x >= frame_width || y >= frame_height) {
1317 auto *row =
static_cast<std::uint8_t *
>(frame_surface->pixels) + (
static_cast<std::size_t
>(y) *
static_cast<std::size_t
>(frame_surface->pitch));
1318 auto *pixel =
reinterpret_cast<std::uint32_t *
>(row) + x;
1319 *pixel = mapColor(color);
1323 static constexpr std::array<mxvk::MXCOLOR, 10> colors{{
1335 return colors[
static_cast<std::size_t
>(std::clamp(color, 0, 9))];
1338 [[nodiscard]]
static mxvk::vec4D projectCubePoint(
const mxvk::vec4D &point,
const CubeInstance &cube) {
1339 const float scale =
static_cast<float>(cube.size) * 2.25f;
1340 const float z = std::max(point.
z, 0.001f);
1342 static_cast<float>(cube.center_x) + (point.
x / z) * scale,
1343 static_cast<float>(cube.center_y) - (point.
y / z) * scale,
1349 void drawCube(
const CubeInstance &cube) {
1350 const float time =
static_cast<float>(SDL_GetTicks()) * 0.001f;
1351 const float flash_scale = cube.flashing ? 0.72f + std::sin(time * 24.0f) * 0.28f : 1.0f;
1353 const std::array<mxvk::vec4D, 8> cube_vertices = {
1354 mxvk::vec4D{-1.0f, -1.0f, -1.0f, 1.0f},
1355 mxvk::vec4D{1.0f, -1.0f, -1.0f, 1.0f},
1356 mxvk::vec4D{1.0f, 1.0f, -1.0f, 1.0f},
1357 mxvk::vec4D{-1.0f, 1.0f, -1.0f, 1.0f},
1358 mxvk::vec4D{-1.0f, -1.0f, 1.0f, 1.0f},
1359 mxvk::vec4D{1.0f, -1.0f, 1.0f, 1.0f},
1360 mxvk::vec4D{1.0f, 1.0f, 1.0f, 1.0f},
1361 mxvk::vec4D{-1.0f, 1.0f, 1.0f, 1.0f},
1363 const std::array<std::array<int, 4>, 6> cube_faces = {{
1372 mxvk::Mat4D rotation;
1373 rotation.
BuildXYZ(0.0f, time * 112.0f + cube.phase, 0.0f);
1375 std::array<mxvk::vec4D, 8> camera_vertices{};
1376 std::array<mxvk::vec4D, 8> projected{};
1377 for (std::size_t i = 0; i < cube_vertices.size(); ++i) {
1378 mxvk::vec4D point = rotation.
MulVec(cube_vertices[i]);
1380 camera_vertices[i] = point;
1381 projected[i] = projectCubePoint(point, cube);
1384 mxvk::vec3D light_dir(-0.35f, -0.55f, -1.0f);
1385 light_dir.Normalize();
1387 std::vector<FaceDraw> faces;
1388 faces.reserve(cube_faces.size());
1389 for (
const auto &indices : cube_faces) {
1390 const mxvk::vec4D &a = camera_vertices[
static_cast<std::size_t
>(indices[0])];
1391 const mxvk::vec4D &b = camera_vertices[
static_cast<std::size_t
>(indices[1])];
1392 const mxvk::vec4D &c = camera_vertices[
static_cast<std::size_t
>(indices[2])];
1393 mxvk::vec4D normal = mxvk::vec4D().Build(a, b).CrossProduct(mxvk::vec4D().Build(a, c));
1396 const mxvk::vec4D center = (a + b + c + camera_vertices[
static_cast<std::size_t
>(indices[3])]) * 0.25f;
1397 const mxvk::vec4D view_vector(-center.
x, -center.
y, -center.
z, 1.0f);
1398 if (normal.DotProduct(view_vector) <= 0.0f) {
1402 const float diffuse = std::max(0.0f, normal.DotProduct(mxvk::vec4D(light_dir.x, light_dir.y, light_dir.z, 1.0f)));
1403 const float intensity = std::clamp(0.28f + diffuse * 0.72f, 0.0f, 1.0f);
1407 std::ranges::sort(faces, [](
const FaceDraw &left,
const FaceDraw &right) {
return left.depth > right.depth; });
1409 for (
const FaceDraw &face : faces) {
1410 mxvk::PipeLine fill_pipeline;
1411 fill_pipeline.
Begin(frame_width, frame_height, [
this](
int x,
int y,
mxvk::MXCOLOR color) { putPixel(x, y, color); });
1412 const mxvk::vec4D &a = projected[
static_cast<std::size_t
>(face.indices[0])];
1413 const mxvk::vec4D &b = projected[
static_cast<std::size_t
>(face.indices[1])];
1414 const mxvk::vec4D &c = projected[
static_cast<std::size_t
>(face.indices[2])];
1415 const mxvk::vec4D &d = projected[
static_cast<std::size_t
>(face.indices[3])];
1418 fill_pipeline.
End();
1422 void drawHud(
float scaleX,
float scaleY) {
1423 printText(std::format(
"Score: {}", score),
static_cast<int>(200.0f * scaleX),
static_cast<int>(80.0f * scaleY) - 24, SDL_Color{255, 255, 255, 255}, ui_font);
1424 printText(std::format(
"Tabs: {}", lines),
static_cast<int>(310.0f * scaleX),
static_cast<int>(80.0f * scaleY) - 24, SDL_Color{255, 255, 255, 255}, ui_font);
1427 void handleControllerAxis(Sint16 lx, Sint16 ly) {
1428 const Uint32 now = SDL_GetTicks();
1431 if (ly < -joystick_dead_zone) {
1432 if (now - joy_repeat_up_ms > joy_repeat_delay_ms) {
1433 menu_selection = (menu_selection +
menu_item_count - 1) % menu_item_count;
1434 joy_repeat_up_ms = now;
1437 joy_repeat_up_ms = 0U;
1440 if (ly > joystick_dead_zone) {
1441 if (now - joy_repeat_down_ms > joy_repeat_delay_ms) {
1442 menu_selection = (menu_selection + 1) % menu_item_count;
1443 joy_repeat_down_ms = now;
1446 joy_repeat_down_ms = 0U;
1451 if (screen !=
Screen::Game || paused || awaiting_name) {
1455 if (lx < -joystick_dead_zone) {
1456 if (now - joy_repeat_left_ms > joy_repeat_delay_ms) {
1458 joy_repeat_left_ms = now;
1461 joy_repeat_left_ms = 0U;
1464 if (lx > joystick_dead_zone) {
1465 if (now - joy_repeat_right_ms > joy_repeat_delay_ms) {
1467 joy_repeat_right_ms = now;
1470 joy_repeat_right_ms = 0U;
1473 if (ly > joystick_dead_zone) {
1474 if (now - joy_repeat_down_ms > joy_repeat_delay_ms) {
1475 if (!movePiece(0, 1)) {
1478 joy_repeat_down_ms = now;
1481 joy_repeat_down_ms = 0U;
1484 if (lx == 0 && ly == 0) {
1485 joy_repeat_up_ms = 0U;
1489 void pollController([[maybe_unused]] Uint64 now) {
1490 if (gamepad ==
nullptr) {
1494 const Sint16 lx = SDL_GetGamepadAxis(gamepad, SDL_GAMEPAD_AXIS_LEFTX);
1495 const Sint16 ly = SDL_GetGamepadAxis(gamepad, SDL_GAMEPAD_AXIS_LEFTY);
1496 handleControllerAxis(lx, ly);
1499 bool openGamepad(SDL_JoystickID
id) {
1500 if (gamepad !=
nullptr && gamepadId ==
id) {
1505 gamepad = SDL_OpenGamepad(
id);
1506 if (gamepad ==
nullptr) {
1514 void tryOpenFirstGamepad() {
1515 if (gamepad !=
nullptr) {
1520 SDL_JoystickID *ids = SDL_GetGamepads(&count);
1521 if (ids ==
nullptr || count <= 0) {
1522 if (ids !=
nullptr) {
1528 openGamepad(ids[0]);
1532 void closeGamepad() {
1533 if (gamepad !=
nullptr) {
1534 SDL_CloseGamepad(gamepad);