60 Uint64 flash_until = 0;
66 std::array<int, piece_height>
colors{};
76 for (
int i = 1; i < argc; ++i) {
77 const std::string arg = argv[i] ==
nullptr ? std::string{} : std::string(argv[i]);
78 if (arg ==
"-r" || arg ==
"-R" || arg ==
"--resolution") {
86 if (!path.empty() && path !=
"." && path !=
"./") {
87 return std::filesystem::path(path);
89 return std::filesystem::path(MASTERPIECE_ASSET_DIR);
93 const std::filesystem::path shared_root = asset_root.parent_path() /
"puzzle";
94 if (std::filesystem::exists(shared_root /
"data" /
"gamebg.png")) {
100 std::filesystem::path
scorePath(
const std::filesystem::path &asset_root) {
101 return asset_root /
"data" /
"scores.dat";
107 : file_path(std::move(file_path)) {
111 void add(std::string name,
int score) {
113 entries.push_back({std::move(name), score});
122 return score > entries.back().score;
125 [[nodiscard]]
const std::vector<ScoreEntry> &
list()
const {
130 std::filesystem::path file_path;
131 std::vector<ScoreEntry> entries;
133 static void normalize(std::string &name) {
135 cleaned.reserve(name.size());
136 for (
unsigned char ch : name) {
137 if (ch >= 32 && ch < 127 && ch !=
':') {
138 cleaned.push_back(
static_cast<char>(ch));
142 if (cleaned.empty()) {
145 if (cleaned.size() > max_name_length) {
146 cleaned.resize(max_name_length);
148 name = std::move(cleaned);
152 std::sort(entries.begin(), entries.end(), [](
const ScoreEntry &a,
const ScoreEntry &b) {
153 if (a.score != b.score) {
154 return a.score > b.score;
156 return a.name < b.name;
159 if (entries.size() > max_scores) {
160 entries.resize(max_scores);
164 void initDefaults() {
167 entries.push_back({
"Anonymous", 0});
174 std::ifstream in(file_path);
181 while (std::getline(in, line)) {
182 const std::size_t sep = line.find(
':');
183 if (sep == std::string::npos) {
188 entry.name = line.substr(0, sep);
189 entry.score =
static_cast<int>(std::strtol(line.substr(sep + 1).c_str(),
nullptr, 10));
190 normalize(entry.name);
191 entries.push_back(std::move(entry));
194 if (entries.empty()) {
205 std::filesystem::create_directories(file_path.parent_path(), ec);
207 std::ofstream out(file_path, std::ios::trunc);
208 if (!out.is_open()) {
212 for (
const ScoreEntry &entry : entries) {
213 out << entry.name <<
':' << entry.score <<
'\n';
247 MasterPieceWindow(
const std::string &path,
int width,
int height,
bool fullscreen,
bool enable_vsync)
249 asset_root(resolveAssetRoot(path)),
250 puzzle_asset_root(resolvePuzzleAssetRoot(resolveAssetRoot(path))),
251 high_scores(scorePath(asset_root)) {
254 const std::string
font_path = dataPath(
"font.ttf");
262 tryOpenFirstGamepad();
270 if (e.type == SDL_EVENT_QUIT) {
275 if (e.type == SDL_EVENT_GAMEPAD_ADDED) {
276 openGamepad(e.gdevice.which);
280 if (e.type == SDL_EVENT_GAMEPAD_REMOVED) {
281 if (gamepad !=
nullptr && e.gdevice.which == gamepadId) {
283 tryOpenFirstGamepad();
288 if (e.type == SDL_EVENT_GAMEPAD_BUTTON_DOWN) {
289 handleControllerButton(e.gbutton.button);
294 handleNameText(e.text.text);
298 if (e.type != SDL_EVENT_KEY_DOWN) {
304 if (isConfirmKey(e.key.key) || e.key.key == SDLK_ESCAPE) {
309 handleMenuKey(e.key.key);
312 handleGameKey(e.key.key);
316 if (e.key.key == SDLK_RETURN || e.key.key == SDLK_ESCAPE) {
321 handleNameKey(e.key.key);
327 const Uint64 now = SDL_GetTicks();
328 layout = computeLayout();
333 if (!drawIntro(now)) {
368 static constexpr std::array<const char *, 11> block_files{{
382 static constexpr std::array<const char *, menu_item_count> menu_files{{
384 "menu_high_scores.png",
389 std::filesystem::path asset_root;
390 std::filesystem::path puzzle_asset_root;
394 std::mt19937 rng{std::random_device{}()};
395 std::array<std::array<Cell, board_cols>,
board_rows> board{};
397 mxvk::Font title_font{};
398 mxvk::Font ui_font{};
399 mxvk::VK_Sprite *background_intro =
nullptr;
400 mxvk::VK_Sprite *mxvk_logo =
nullptr;
401 mxvk::VK_Sprite *background_menu =
nullptr;
402 mxvk::VK_Sprite *background_game =
nullptr;
403 mxvk::VK_Sprite *cursor =
nullptr;
404 std::array<mxvk::VK_Sprite *, block_files.size()> blocks{};
405 std::array<mxvk::VK_Sprite *, menu_files.size()> menu_items{};
406 mxvk::VK_Sprite *panel =
nullptr;
407 mxvk::VK_Sprite *overlay =
nullptr;
408 std::string player_name;
409 int menu_selection = 0;
413 int lines_toward_speedup = 0;
414 int fall_delay_ms = 520;
415 Uint64 intro_start_ms = 0;
416 Uint64 last_update_ms = 0;
417 Uint64 fall_accumulator_ms = 0;
418 Uint32 joy_repeat_left_ms = 0;
419 Uint32 joy_repeat_right_ms = 0;
420 Uint32 joy_repeat_up_ms = 0;
421 Uint32 joy_repeat_down_ms = 0;
422 SDL_Gamepad *gamepad =
nullptr;
423 SDL_JoystickID gamepadId = 0;
425 bool awaiting_name =
false;
426 bool score_added =
false;
427 bool waiting_for_spawn =
false;
429 static bool isConfirmKey(SDL_Keycode key) {
430 return key == SDLK_RETURN || key == SDLK_SPACE;
433 static int scaled(
int value,
float scale) {
434 return std::max(1,
static_cast<int>(std::lround(
static_cast<float>(value) * scale)));
437 static int scaledPos(
int value,
float scale) {
438 return static_cast<int>(std::lround(
static_cast<float>(value) * scale));
441 static int flashingSpriteIndex(
int x,
int y, Uint64 now) {
442 const Uint64 tick = now / 18U;
443 const Uint64 mixed = tick +
static_cast<Uint64
>(x * 37 + y * 101);
444 return static_cast<int>((mixed % 9U) + 1U);
447 [[nodiscard]] Layout computeLayout()
const {
450 result.width = extent.width == 0U ?
base_width :
static_cast<int>(extent.width);
451 result.height = extent.height == 0U ?
base_height :
static_cast<int>(extent.height);
452 result.scale_x =
static_cast<float>(result.width) /
static_cast<float>(base_width);
453 result.scale_y =
static_cast<float>(result.height) /
static_cast<float>(base_height);
454 result.game_scale = std::min(result.scale_x, result.scale_y);
455 result.game_w = scaled(base_width, result.game_scale);
456 result.game_h = scaled(base_height, result.game_scale);
457 result.game_x = (result.width - result.game_w) / 2;
458 result.game_y = (result.height - result.game_h) / 2;
459 result.board_x = result.game_x + scaledPos(185, result.game_scale);
460 result.board_y = result.game_y + scaledPos(95, result.game_scale);
461 result.cell_w = scaled(32, result.game_scale);
462 result.cell_h = scaled(16, result.game_scale);
463 result.next_x = result.game_x + scaledPos(510, result.game_scale);
464 result.next_y = result.game_y + scaledPos(200, result.game_scale);
465 result.menu_x = scaled(505, result.scale_x);
466 result.menu_y = scaled(400, result.scale_y);
467 result.menu_w = scaled(430, result.scale_x);
468 result.menu_h = scaled(82, result.scale_y);
469 result.menu_step = scaled(118, result.scale_y);
473 std::string dataPath(
const char *name)
const {
474 return (asset_root /
"data" / name).string();
477 std::string puzzleDataPath(
const char *name)
const {
478 const std::filesystem::path shared_path = puzzle_asset_root /
"data" / name;
479 if (std::filesystem::exists(shared_path)) {
480 return shared_path.string();
482 return dataPath(name);
485 mxvk::VK_Sprite *loadPngSprite(
const char *name) {
489 mxvk::VK_Sprite *loadEffectSprite(
const char *name) {
490 return createSprite(dataPath(name),
"", dataPath(
"intro.frag.spv"));
493 mxvk::VK_Sprite *makeSolidPixel(std::uint8_t r, std::uint8_t g, std::uint8_t b, std::uint8_t a) {
494 const std::array<std::uint8_t, 4> pixel{r, g, b, a};
501 background_intro = loadEffectSprite(
"intro.png");
502 background_menu = loadEffectSprite(
"start.png");
503 background_game =
createSprite(puzzleDataPath(
"gamebg.png"));
504 mxvk_logo =
createSprite(puzzleDataPath(
"mxvk_logo.png"));
505 cursor = loadPngSprite(
"cursor.png");
507 for (std::size_t i = 0; i < block_files.size(); ++i) {
508 blocks[i] = loadPngSprite(block_files[i]);
511 for (std::size_t i = 0; i < menu_files.size(); ++i) {
512 menu_items[i] = loadPngSprite(menu_files[i]);
515 panel = makeSolidPixel(0, 0, 0, 192);
516 overlay = makeSolidPixel(0, 0, 0, 128);
519 void setScreen(Screen next) {
521 SDL_StopTextInput(
window.get());
529 SDL_StartTextInput(
window.get());
534 intro_start_ms = SDL_GetTicks();
539 for (
auto &row : board) {
540 for (Cell &cell : row) {
548 lines_toward_speedup = 0;
551 fall_accumulator_ms = 0;
553 awaiting_name =
false;
555 waiting_for_spawn =
false;
558 piece.next_colors = randomColors();
562 std::array<int, piece_height> randomColors() {
563 std::uniform_int_distribution<int> dist(1, 9);
564 std::array<int, piece_height> colors{dist(rng), dist(rng), dist(rng)};
565 while (colors[0] == colors[1] && colors[1] == colors[2]) {
566 colors[1] = dist(rng);
571 bool canPlacePiece(
int x,
int y)
const {
572 if (x < 0 || x >= board_cols) {
577 const int row = y + i;
578 if (row < 0 || row >= board_rows) {
581 if (board[
static_cast<std::size_t
>(row)][
static_cast<std::size_t
>(x)].color != 0) {
588 bool movePiece(
int dx,
int dy) {
589 const int next_x = piece.x + dx;
590 const int next_y = piece.y + dy;
591 if (!canPlacePiece(next_x, next_y)) {
601 while (movePiece(0, 1)) {
603 lockPiece(SDL_GetTicks());
606 void rotatePieceColors(
bool forward) {
608 const int temp = piece.colors.back();
609 piece.colors[2] = piece.colors[1];
610 piece.colors[1] = piece.colors[0];
611 piece.colors[0] = temp;
613 const int temp = piece.colors.front();
614 piece.colors[0] = piece.colors[1];
615 piece.colors[1] = piece.colors[2];
616 piece.colors[2] = temp;
620 void handleControllerButton(Uint8 button) {
623 if (button == SDL_GAMEPAD_BUTTON_SOUTH || button == SDL_GAMEPAD_BUTTON_START) {
628 if (button == SDL_GAMEPAD_BUTTON_DPAD_UP) {
629 menu_selection = (menu_selection +
menu_item_count - 1) % menu_item_count;
630 }
else if (button == SDL_GAMEPAD_BUTTON_DPAD_DOWN) {
631 menu_selection = (menu_selection + 1) % menu_item_count;
632 }
else if (button == SDL_GAMEPAD_BUTTON_SOUTH || button == SDL_GAMEPAD_BUTTON_START) {
633 handleMenuSelection();
634 }
else if (button == SDL_GAMEPAD_BUTTON_EAST || button == SDL_GAMEPAD_BUTTON_BACK) {
639 if (button == SDL_GAMEPAD_BUTTON_BACK) {
641 }
else if (button == SDL_GAMEPAD_BUTTON_START) {
643 }
else if (paused || awaiting_name) {
645 }
else if (button == SDL_GAMEPAD_BUTTON_DPAD_LEFT) {
647 }
else if (button == SDL_GAMEPAD_BUTTON_DPAD_RIGHT) {
649 }
else if (button == SDL_GAMEPAD_BUTTON_DPAD_DOWN) {
650 if (!movePiece(0, 1)) {
651 lockPiece(SDL_GetTicks());
653 }
else if (button == SDL_GAMEPAD_BUTTON_DPAD_UP || button == SDL_GAMEPAD_BUTTON_SOUTH) {
654 rotatePieceColors(
true);
655 }
else if (button == SDL_GAMEPAD_BUTTON_EAST) {
656 rotatePieceColors(
false);
657 }
else if (button == SDL_GAMEPAD_BUTTON_NORTH) {
663 if (button == SDL_GAMEPAD_BUTTON_SOUTH || button == SDL_GAMEPAD_BUTTON_START) {
665 }
else if (button == SDL_GAMEPAD_BUTTON_EAST || button == SDL_GAMEPAD_BUTTON_BACK) {
667 SDL_StopTextInput(
window.get());
670 }
else if (button == SDL_GAMEPAD_BUTTON_SOUTH || button == SDL_GAMEPAD_BUTTON_START ||
671 button == SDL_GAMEPAD_BUTTON_EAST || button == SDL_GAMEPAD_BUTTON_BACK) {
676 if (button == SDL_GAMEPAD_BUTTON_SOUTH || button == SDL_GAMEPAD_BUTTON_START ||
677 button == SDL_GAMEPAD_BUTTON_EAST || button == SDL_GAMEPAD_BUTTON_BACK) {
682 if (button == SDL_GAMEPAD_BUTTON_SOUTH || button == SDL_GAMEPAD_BUTTON_START) {
684 }
else if (button == SDL_GAMEPAD_BUTTON_EAST || button == SDL_GAMEPAD_BUTTON_BACK) {
686 SDL_StopTextInput(
window.get());
693 void handleMenuSelection() {
694 switch (menu_selection) {
713 void handleMenuKey(SDL_Keycode key) {
714 if (key == SDLK_ESCAPE) {
719 if (key == SDLK_UP) {
720 menu_selection = (menu_selection +
menu_item_count - 1) % menu_item_count;
724 if (key == SDLK_DOWN) {
725 menu_selection = (menu_selection + 1) % menu_item_count;
729 if (!isConfirmKey(key)) {
733 handleMenuSelection();
736 void handleGameKey(SDL_Keycode key) {
737 if (key == SDLK_ESCAPE) {
742 if (key ==
'p' || key ==
'P') {
747 if (paused || awaiting_name) {
751 if (key == SDLK_LEFT) {
753 }
else if (key == SDLK_RIGHT) {
755 }
else if (key == SDLK_DOWN) {
756 if (!movePiece(0, 1)) {
757 lockPiece(SDL_GetTicks());
759 }
else if (key ==
'a' || key ==
'A' || key == SDLK_UP) {
760 rotatePieceColors(
true);
761 }
else if (key ==
's' || key ==
'S') {
762 rotatePieceColors(
false);
766 void handleNameKey(SDL_Keycode key) {
767 if (key == SDLK_ESCAPE) {
769 SDL_StopTextInput(
window.get());
774 if (key == SDLK_BACKSPACE) {
775 if (!player_name.empty()) {
776 player_name.pop_back();
781 if (key == SDLK_RETURN) {
786 void handleNameText(
const char *text) {
787 if (text ==
nullptr) {
791 while (*text !=
'\0' &&
static_cast<int>(player_name.size()) < max_name_length) {
792 const unsigned char ch =
static_cast<unsigned char>(*text++);
793 if (ch >= 32 && ch < 127) {
794 player_name.push_back(
static_cast<char>(ch));
801 if (player_name.empty()) {
802 player_name =
"Player";
804 high_scores.add(player_name, score);
808 SDL_StopTextInput(
window.get());
813 piece.colors = piece.next_colors;
814 piece.next_colors = randomColors();
818 if (!canPlacePiece(piece.x, piece.y)) {
823 bool boardHasFlashCells()
const {
824 for (
const auto &row : board) {
825 for (
const Cell &cell : row) {
826 if (cell.flash_until != 0U) {
834 bool updateFlashState(Uint64 now) {
835 if (!boardHasFlashCells()) {
839 bool expired_any =
false;
840 for (
auto &row : board) {
841 for (Cell &cell : row) {
842 if (cell.flash_until != 0U && now >= cell.flash_until) {
849 if (boardHasFlashCells()) {
858 if (!boardHasFlashCells() && waiting_for_spawn) {
859 waiting_for_spawn =
false;
866 void updateGame(Uint64 now) {
867 if (paused || awaiting_name) {
871 if (last_update_ms == 0U) {
872 last_update_ms = now;
876 if (updateFlashState(now)) {
877 last_update_ms = now;
881 if (waiting_for_spawn) {
882 waiting_for_spawn =
false;
884 last_update_ms = now;
888 const Uint64 delta = now - last_update_ms;
889 last_update_ms = now;
890 fall_accumulator_ms += delta;
892 while (fall_accumulator_ms >=
static_cast<Uint64
>(fall_delay_ms)) {
893 fall_accumulator_ms -=
static_cast<Uint64
>(fall_delay_ms);
894 if (!movePiece(0, 1)) {
901 bool resolveMatches(Uint64 now) {
902 std::array<std::array<bool, board_cols>,
board_rows> marked{};
903 int matches_found = 0;
905 auto mark_run = [&](
int x,
int y,
int dx,
int dy) {
906 const int color = board[
static_cast<std::size_t
>(y)][
static_cast<std::size_t
>(x)].color;
911 while (cx >= 0 && cy >= 0 && cx < board_cols && cy < board_rows &&
912 board[
static_cast<std::size_t
>(cy)][
static_cast<std::size_t
>(cx)].color == color &&
913 board[
static_cast<std::size_t
>(cy)][
static_cast<std::size_t
>(cx)].flash_until == 0U) {
926 for (
int i = 0; i < length; ++i) {
927 marked[
static_cast<std::size_t
>(cy)][
static_cast<std::size_t
>(cx)] =
true;
935 const Cell &cell = board[
static_cast<std::size_t
>(y)][
static_cast<std::size_t
>(x)];
936 if (cell.color == 0 || cell.flash_until != 0U) {
940 if (x == 0 || board[
static_cast<std::size_t
>(y)][
static_cast<std::size_t
>(x - 1)].color != cell.color) {
941 mark_run(x, y, 1, 0);
943 if (y == 0 || board[
static_cast<std::size_t
>(y - 1)][
static_cast<std::size_t
>(x)].color != cell.color) {
944 mark_run(x, y, 0, 1);
946 if (x == 0 || y == 0 ||
947 board[
static_cast<std::size_t
>(y - 1)][
static_cast<std::size_t
>(x - 1)].color != cell.color) {
948 mark_run(x, y, 1, 1);
950 if (x == board_cols - 1 || y == 0 ||
951 board[
static_cast<std::size_t
>(y - 1)][
static_cast<std::size_t
>(x + 1)].color != cell.color) {
952 mark_run(x, y, -1, 1);
957 if (matches_found == 0) {
963 if (marked[
static_cast<std::size_t
>(y)][
static_cast<std::size_t
>(x)]) {
964 board[
static_cast<std::size_t
>(y)][
static_cast<std::size_t
>(x)].flash_until = now +
flash_time_ms;
970 lines += matches_found;
971 lines_toward_speedup += matches_found;
972 while (lines_toward_speedup >= lines_per_speedup) {
975 fall_delay_ms = std::max(140, 520 - speed_level * 40);
981 void applyGravity() {
984 for (
int y = board_rows - 1; y >= 0; --y) {
985 if (board[
static_cast<std::size_t
>(y)][
static_cast<std::size_t
>(x)].color != 0) {
986 if (write_row != y) {
987 board[
static_cast<std::size_t
>(write_row)][
static_cast<std::size_t
>(x)] =
988 board[
static_cast<std::size_t
>(y)][
static_cast<std::size_t
>(x)];
989 board[
static_cast<std::size_t
>(y)][
static_cast<std::size_t
>(x)] = {};
995 for (
int y = write_row; y >= 0; --y) {
996 board[
static_cast<std::size_t
>(y)][
static_cast<std::size_t
>(x)] = {};
1001 void lockPiece(Uint64 now) {
1008 const int row = piece.y + i;
1009 board[
static_cast<std::size_t
>(row)][
static_cast<std::size_t
>(piece.x)].color =
1010 std::clamp(piece.colors[
static_cast<std::size_t
>(i)], 1, 9);
1013 if (resolveMatches(now)) {
1014 waiting_for_spawn =
true;
1021 void handleGameOver() {
1022 if (high_scores.qualifies(score)) {
1023 score_added =
false;
1024 player_name.clear();
1031 void drawSprite(mxvk::VK_Sprite *sprite,
int x,
int y,
int w,
int h) {
1032 if (sprite !=
nullptr) {
1037 void drawCenteredText(
const std::string &text,
int y,
const SDL_Color &color,
const mxvk::Font &font) {
1041 printText(text, scaled(32, layout.scale_x), y, color, font);
1045 const int x = std::max(16, (layout.width - w) / 2);
1049 bool drawIntro(Uint64 now) {
1050 const float elapsed = intro_start_ms == 0U ? 0.0f :
static_cast<float>(now - intro_start_ms) / 1000.0f;
1051 background_intro->setShaderParams(elapsed, 0.0f, 0.0f, 1.0f);
1052 drawSprite(background_intro, 0, 0, layout.width, layout.height);
1054 if (intro_start_ms == 0U) {
1055 intro_start_ms = now;
1058 if (now - intro_start_ms > title_screen_time_ms) {
1067 const float elapsed =
static_cast<float>(SDL_GetTicks()) / 1000.0f;
1068 background_menu->setShaderParams(elapsed, 0.0f, 0.0f, 1.0f);
1069 drawSprite(background_menu, 0, 0, layout.width, layout.height);
1072 const int y = layout.menu_y + i * layout.menu_step;
1073 if (i == menu_selection) {
1074 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));
1076 drawSprite(menu_items[
static_cast<std::size_t
>(i)], layout.menu_x, y, layout.menu_w, layout.menu_h);
1080 void drawScores(
bool entering_name) {
1081 const float elapsed =
static_cast<float>(SDL_GetTicks()) / 1000.0f;
1082 background_menu->setShaderParams(elapsed, 0.0f, 0.0f, 1.0f);
1083 drawSprite(background_menu, 0, 0, layout.width, layout.height);
1084 drawSprite(overlay, scaled(30, layout.scale_x), scaled(70, layout.scale_y),
1085 layout.width - scaled(60, layout.scale_x), layout.height - scaled(120, layout.scale_y));
1087 drawCenteredText(
"High Scores", scaled(72, layout.scale_y), SDL_Color{255, 245, 200, 255}, title_font);
1089 const auto &entries = high_scores.list();
1090 const int start_y = scaled(140, layout.scale_y);
1091 const int step_y = scaled(30, layout.scale_y);
1092 for (std::size_t i = 0; i < entries.size(); ++i) {
1093 const std::string line = std::format(
"{:>2}. {:<16} {}", i + 1, entries[i].name, entries[i].score);
1094 printText(line, scaled(70, layout.scale_x), start_y +
static_cast<int>(i) * step_y, SDL_Color{255, 255, 255, 255}, ui_font);
1097 if (entering_name) {
1098 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);
1099 printText(
"Name:", scaled(70, layout.scale_x), scaled(490, layout.scale_y), SDL_Color{255, 245, 200, 255}, ui_font);
1100 printText(player_name +
"_", scaled(150, layout.scale_x), scaled(490, layout.scale_y), SDL_Color{255, 255, 255, 255}, ui_font);
1102 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);
1106 void drawCredits() {
1107 const float elapsed =
static_cast<float>(SDL_GetTicks()) / 1000.0f;
1108 background_menu->setShaderParams(elapsed, 0.0f, 0.0f, 1.0f);
1109 drawSprite(background_menu, 0, 0, layout.width, layout.height);
1110 drawSprite(overlay, scaled(30, layout.scale_x), scaled(85, layout.scale_y),
1111 layout.width - scaled(60, layout.scale_x), scaled(260, layout.scale_y));
1112 const int logo_w = layout.width / 2;
1113 const int logo_h =
static_cast<int>(std::lround(
1114 static_cast<float>(logo_w) *
static_cast<float>(mxvk_logo->getHeight()) /
1115 static_cast<float>(mxvk_logo->getWidth())));
1116 const int logo_x = (layout.width - logo_w) / 2;
1117 const int logo_y = (layout.height - logo_h) / 2;
1118 drawSprite(mxvk_logo, logo_x, logo_y, logo_w, logo_h);
1119 drawCenteredText(
"Credits", scaled(96, layout.scale_y), SDL_Color{255, 245, 200, 255}, title_font);
1120 printText(
"Original game: MasterPiece.SDL", scaled(70, layout.scale_x), scaled(180, layout.scale_y), SDL_Color{255, 255, 255, 255}, ui_font);
1121 printText(
"MXVK port and cleanup: 2D Vulkan example", scaled(70, layout.scale_x), scaled(214, layout.scale_y), SDL_Color{255, 255, 255, 255}, ui_font);
1122 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);
1125 void drawGame(Uint64 now) {
1126 const float scaleX =
static_cast<float>(layout.width) /
static_cast<float>(game_base_width);
1127 const float scaleY =
static_cast<float>(layout.height) /
static_cast<float>(game_base_height);
1128 drawSprite(background_game, 0, 0, layout.width, layout.height);
1129 drawBoard(now, scaleX, scaleY);
1130 drawNextPiece(scaleX, scaleY);
1131 drawHud(scaleX, scaleY);
1134 const char *pausedText =
"PAUSED - Press P to Continue";
1135 int pausedWidth = 0;
1136 int pausedHeight = 0;
1138 pausedWidth =
static_cast<int>(std::strlen(pausedText)) * 16;
1140 printText(pausedText, layout.width / 2 - pausedWidth / 2, layout.height / 2, SDL_Color{255, 255, 0, 255}, title_font);
1144 void drawBoard(Uint64 now,
float scaleX,
float scaleY) {
1147 const Cell &cell = board[
static_cast<std::size_t
>(j)][
static_cast<std::size_t
>(i)];
1148 if (cell.color == 0) {
1152 const int sprite_index = cell.flash_until != 0U ? flashingSpriteIndex(i, j, now) : std::clamp(cell.color, 0, 9);
1156 blocks[
static_cast<std::size_t
>(sprite_index)]->drawSpriteRect(
1157 static_cast<int>(
static_cast<float>(x) * scaleX),
1158 static_cast<int>(
static_cast<float>(y) * scaleY) + 10,
1159 static_cast<int>(
static_cast<float>(game_block_width) * scaleX),
1160 static_cast<int>(
static_cast<float>(game_block_height) * scaleY));
1169 const int row = piece.y + i;
1170 if (row < 0 || row >= board_rows || piece.x < 0 || piece.x >= board_cols) {
1176 blocks[
static_cast<std::size_t
>(std::clamp(piece.colors[
static_cast<std::size_t
>(i)], 0, 9))]->drawSpriteRect(
1177 static_cast<int>(
static_cast<float>(x) * scaleX),
1178 static_cast<int>(
static_cast<float>(y) * scaleY) + 10,
1179 static_cast<int>(
static_cast<float>(game_block_width) * scaleX),
1180 static_cast<int>(
static_cast<float>(game_block_height) * scaleY));
1184 void drawNextPiece(
float scaleX,
float scaleY) {
1189 const int sprite_index = std::clamp(piece.next_colors[
static_cast<std::size_t
>(i)], 0, 9);
1190 blocks[
static_cast<std::size_t
>(sprite_index)]->drawSpriteRect(
1191 static_cast<int>(
static_cast<float>(bx) * scaleX),
1192 static_cast<int>(
static_cast<float>(by + i * (game_block_height + game_block_spacing)) * scaleY),
1193 static_cast<int>(
static_cast<float>(game_block_width) * scaleX),
1194 static_cast<int>(
static_cast<float>(game_block_height) * scaleY));
1198 void drawHud(
float scaleX,
float scaleY) {
1199 printText(std::format(
"Score: {}", score),
1200 static_cast<int>(200.0f * scaleX),
1201 static_cast<int>(80.0f * scaleY) - 24,
1202 SDL_Color{255, 255, 255, 255},
1204 printText(std::format(
"Tabs: {}", lines),
1205 static_cast<int>(310.0f * scaleX),
1206 static_cast<int>(80.0f * scaleY) - 24,
1207 SDL_Color{255, 255, 255, 255},
1211 void handleControllerAxis(Sint16 lx, Sint16 ly) {
1212 const Uint32 now = SDL_GetTicks();
1215 if (ly < -joystick_dead_zone) {
1216 if (now - joy_repeat_up_ms > joy_repeat_delay_ms) {
1217 menu_selection = (menu_selection +
menu_item_count - 1) % menu_item_count;
1218 joy_repeat_up_ms = now;
1221 joy_repeat_up_ms = 0U;
1224 if (ly > joystick_dead_zone) {
1225 if (now - joy_repeat_down_ms > joy_repeat_delay_ms) {
1226 menu_selection = (menu_selection + 1) % menu_item_count;
1227 joy_repeat_down_ms = now;
1230 joy_repeat_down_ms = 0U;
1235 if (screen !=
Screen::Game || paused || awaiting_name) {
1239 if (lx < -joystick_dead_zone) {
1240 if (now - joy_repeat_left_ms > joy_repeat_delay_ms) {
1242 joy_repeat_left_ms = now;
1245 joy_repeat_left_ms = 0U;
1248 if (lx > joystick_dead_zone) {
1249 if (now - joy_repeat_right_ms > joy_repeat_delay_ms) {
1251 joy_repeat_right_ms = now;
1254 joy_repeat_right_ms = 0U;
1257 if (ly > joystick_dead_zone) {
1258 if (now - joy_repeat_down_ms > joy_repeat_delay_ms) {
1259 if (!movePiece(0, 1)) {
1262 joy_repeat_down_ms = now;
1265 joy_repeat_down_ms = 0U;
1268 if (lx == 0 && ly == 0) {
1269 joy_repeat_up_ms = 0U;
1273 void pollController([[maybe_unused]] Uint64 now) {
1274 if (gamepad ==
nullptr) {
1278 const Sint16 lx = SDL_GetGamepadAxis(gamepad, SDL_GAMEPAD_AXIS_LEFTX);
1279 const Sint16 ly = SDL_GetGamepadAxis(gamepad, SDL_GAMEPAD_AXIS_LEFTY);
1280 handleControllerAxis(lx, ly);
1283 bool openGamepad(SDL_JoystickID
id) {
1284 if (gamepad !=
nullptr && gamepadId ==
id) {
1289 gamepad = SDL_OpenGamepad(
id);
1290 if (gamepad ==
nullptr) {
1298 void tryOpenFirstGamepad() {
1299 if (gamepad !=
nullptr) {
1304 SDL_JoystickID *ids = SDL_GetGamepads(&count);
1305 if (ids ==
nullptr || count <= 0) {
1306 if (ids !=
nullptr) {
1312 openGamepad(ids[0]);
1316 void closeGamepad() {
1317 if (gamepad !=
nullptr) {
1318 SDL_CloseGamepad(gamepad);
1331 if (!explicit_resolution) {
1339 std::cerr << std::format(
"mxvk: Exception: {}\n", e.
text());
1340 return EXIT_FAILURE;
1342 std::cerr << std::format(
"mxvk: Argument Exception: {}\n", e.
text());
1343 return EXIT_FAILURE;
1346 return EXIT_SUCCESS;
Lightweight, header-only, template command-line argument parser.
Arguments proc_args(int &argc, char **argv)
Parse standard libmx2 command-line options from main()'s argv.
Exception thrown by Argz::proc() on unrecognised or malformed options.
const std::vector< ScoreEntry > & list() const
HighScores(std::filesystem::path file_path)
void add(std::string name, int score)
bool qualifies(int score) const
void event(SDL_Event &e) override
Handle one SDL event.
MasterPieceWindow(const std::string &path, int width, int height, bool fullscreen, bool enable_vsync)
void proc() override
Execute one processing/update step.
~MasterPieceWindow() override
void updateTexture(SDL_Surface *surface)
Replace the sprite texture from an SDL_Surface.
void drawSpriteRect(int x, int y, int w, int h)
Queue a draw into an explicit destination rectangle.
VkExtent2D getSwapchainExtent() const noexcept
Get the current swapchain extent.
void loop()
Run the main event/render loop.
VK_Sprite * createSprite(const std::string &pngPath, const std::string &vertexShaderPath="", const std::string &fragmentShaderPath="")
Create a sprite from a PNG file and register it with this window.
bool getTextDimensions(const std::string &text, int &width, int &height)
Measure text dimensions in pixels.
void setClearColor(float r, float g, float b, float a=1.0f)
Set the per-frame color attachment clear color.
std::unique_ptr< SDL_Window, SDLWindowDeleter > window
void exit()
Request loop termination.
VK_Window()=default
Construct an empty window object.
void setFont(const std::string &fontPath, int fontSize=24)
Set the active text-render font.
void printText(const std::string &text, int x, int y, const SDL_Color &col)
Queue a text string for rendering during the current frame.
std::filesystem::path scorePath(const std::filesystem::path &asset_root)
constexpr int game_block_width
constexpr int piece_height
constexpr int max_name_length
constexpr int game_base_height
std::filesystem::path resolvePuzzleAssetRoot(const std::filesystem::path &asset_root)
constexpr int menu_item_count
constexpr int game_board_start_x
constexpr int flash_time_ms
constexpr int game_next_panel_y
constexpr int game_next_panel_x
constexpr Uint32 joy_repeat_delay_ms
bool hasResolutionArgument(int argc, char **argv)
constexpr int lines_per_speedup
std::filesystem::path resolveAssetRoot(const std::string &path)
constexpr int title_screen_time_ms
constexpr int game_block_height
constexpr int game_block_spacing
constexpr int score_points_per_match
constexpr int game_base_width
constexpr Sint16 joystick_dead_zone
constexpr int base_height
constexpr int game_board_start_y
Utilities for loading and saving PNG images.
Plain data structure returned by proc_args() with all common libmx2 CLI options.
bool fullscreen
Whether fullscreen mode was requested.
bool enable_vsync
Enable FIFO present mode / v-sync (--enable-vsync).
int height
Viewport height in pixels (default: 720).
std::string path
Asset root; proc_args() defaults it to the executable directory.
int width
Viewport width in pixels (default: 1280).
std::array< int, piece_height > next_colors
std::array< int, piece_height > colors