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) {
return asset_root /
"data" /
"scores.dat"; }
104 explicit HighScores(std::filesystem::path file_path) : file_path(std::move(file_path)) { load(); }
106 void add(std::string name,
int score) {
108 entries.push_back({std::move(name), score});
117 return score > entries.back().score;
120 [[nodiscard]]
const std::vector<ScoreEntry> &
list()
const {
return entries; }
123 std::filesystem::path file_path;
124 std::vector<ScoreEntry> entries;
126 static void normalize(std::string &name) {
128 cleaned.reserve(name.size());
129 for (
unsigned char ch : name) {
130 if (ch >= 32 && ch < 127 && ch !=
':') {
131 cleaned.push_back(
static_cast<char>(ch));
135 if (cleaned.empty()) {
138 if (cleaned.size() > max_name_length) {
139 cleaned.resize(max_name_length);
141 name = std::move(cleaned);
145 std::sort(entries.begin(), entries.end(), [](
const ScoreEntry &a,
const ScoreEntry &b) {
146 if (a.score != b.score) {
147 return a.score > b.score;
149 return a.name < b.name;
152 if (entries.size() > max_scores) {
153 entries.resize(max_scores);
157 void initDefaults() {
160 entries.push_back({
"Anonymous", 0});
167 std::ifstream in(file_path);
174 while (std::getline(in, line)) {
175 const std::size_t sep = line.find(
':');
176 if (sep == std::string::npos) {
181 entry.name = line.substr(0, sep);
182 entry.score =
static_cast<int>(std::strtol(line.substr(sep + 1).c_str(),
nullptr, 10));
183 normalize(entry.name);
184 entries.push_back(std::move(entry));
187 if (entries.empty()) {
198 std::filesystem::create_directories(file_path.parent_path(), ec);
200 std::ofstream out(file_path, std::ios::trunc);
201 if (!out.is_open()) {
205 for (
const ScoreEntry &entry : entries) {
206 out << entry.name <<
':' << entry.score <<
'\n';
240 MasterPieceWindow(
const std::string &path,
int width,
int height,
bool fullscreen,
bool enable_vsync) :
mxvk::
VK_Window(
"MasterPiece", width, height, fullscreen,
MXVK_VALIDATION, enable_vsync), asset_root(resolveAssetRoot(path)), puzzle_asset_root(resolvePuzzleAssetRoot(resolveAssetRoot(path))), high_scores(scorePath(asset_root)) {
243 const std::string
font_path = dataPath(
"font.ttf");
251 tryOpenFirstGamepad();
257 if (e.type == SDL_EVENT_QUIT) {
262 if (e.type == SDL_EVENT_GAMEPAD_ADDED) {
263 openGamepad(e.gdevice.which);
267 if (e.type == SDL_EVENT_GAMEPAD_REMOVED) {
268 if (gamepad !=
nullptr && e.gdevice.which == gamepadId) {
270 tryOpenFirstGamepad();
275 if (e.type == SDL_EVENT_GAMEPAD_BUTTON_DOWN) {
276 handleControllerButton(e.gbutton.button);
281 handleNameText(e.text.text);
285 if (e.type != SDL_EVENT_KEY_DOWN) {
291 if (isConfirmKey(e.key.key) || e.key.key == SDLK_ESCAPE) {
296 handleMenuKey(e.key.key);
299 handleGameKey(e.key.key);
303 if (e.key.key == SDLK_RETURN || e.key.key == SDLK_ESCAPE) {
308 handleNameKey(e.key.key);
314 const Uint64 now = SDL_GetTicks();
315 layout = computeLayout();
320 if (!drawIntro(now)) {
355 static constexpr std::array<const char *, 11> block_files{{
369 static constexpr std::array<const char *, menu_item_count> menu_files{{
371 "menu_high_scores.png",
376 std::filesystem::path asset_root;
377 std::filesystem::path puzzle_asset_root;
381 std::mt19937 rng{std::random_device{}()};
382 std::array<std::array<Cell, board_cols>,
board_rows> board{};
384 mxvk::Font title_font{};
385 mxvk::Font ui_font{};
386 mxvk::VK_Sprite *background_intro =
nullptr;
387 mxvk::VK_Sprite *mxvk_logo =
nullptr;
388 mxvk::VK_Sprite *background_menu =
nullptr;
389 mxvk::VK_Sprite *background_game =
nullptr;
390 mxvk::VK_Sprite *cursor =
nullptr;
391 std::array<mxvk::VK_Sprite *, block_files.size()> blocks{};
392 std::array<mxvk::VK_Sprite *, menu_files.size()> menu_items{};
393 mxvk::VK_Sprite *panel =
nullptr;
394 mxvk::VK_Sprite *overlay =
nullptr;
395 std::string player_name;
396 int menu_selection = 0;
400 int lines_toward_speedup = 0;
401 int fall_delay_ms = 520;
402 Uint64 intro_start_ms = 0;
403 Uint64 last_update_ms = 0;
404 Uint64 fall_accumulator_ms = 0;
405 Uint32 joy_repeat_left_ms = 0;
406 Uint32 joy_repeat_right_ms = 0;
407 Uint32 joy_repeat_up_ms = 0;
408 Uint32 joy_repeat_down_ms = 0;
409 SDL_Gamepad *gamepad =
nullptr;
410 SDL_JoystickID gamepadId = 0;
412 bool awaiting_name =
false;
413 bool score_added =
false;
414 bool waiting_for_spawn =
false;
416 static bool isConfirmKey(SDL_Keycode key) {
return key == SDLK_RETURN || key == SDLK_SPACE; }
418 static int scaled(
int value,
float scale) {
return std::max(1,
static_cast<int>(std::lround(
static_cast<float>(value) * scale))); }
420 static int scaledPos(
int value,
float scale) {
return static_cast<int>(std::lround(
static_cast<float>(value) * scale)); }
422 static int flashingSpriteIndex(
int x,
int y, Uint64 now) {
423 const Uint64 tick = now / 18U;
424 const Uint64 mixed = tick +
static_cast<Uint64
>(x * 37 + y * 101);
425 return static_cast<int>((mixed % 9U) + 1U);
428 [[nodiscard]] Layout computeLayout()
const {
431 result.width = extent.width == 0U ?
base_width :
static_cast<int>(extent.width);
432 result.height = extent.height == 0U ?
base_height :
static_cast<int>(extent.height);
433 result.scale_x =
static_cast<float>(result.width) /
static_cast<float>(base_width);
434 result.scale_y =
static_cast<float>(result.height) /
static_cast<float>(base_height);
435 result.game_scale = std::min(result.scale_x, result.scale_y);
436 result.game_w = scaled(base_width, result.game_scale);
437 result.game_h = scaled(base_height, result.game_scale);
438 result.game_x = (result.width - result.game_w) / 2;
439 result.game_y = (result.height - result.game_h) / 2;
440 result.board_x = result.game_x + scaledPos(185, result.game_scale);
441 result.board_y = result.game_y + scaledPos(95, result.game_scale);
442 result.cell_w = scaled(32, result.game_scale);
443 result.cell_h = scaled(16, result.game_scale);
444 result.next_x = result.game_x + scaledPos(510, result.game_scale);
445 result.next_y = result.game_y + scaledPos(200, result.game_scale);
446 result.menu_x = scaled(505, result.scale_x);
447 result.menu_y = scaled(400, result.scale_y);
448 result.menu_w = scaled(430, result.scale_x);
449 result.menu_h = scaled(82, result.scale_y);
450 result.menu_step = scaled(118, result.scale_y);
454 std::string dataPath(
const char *name)
const {
return (asset_root /
"data" / name).string(); }
456 std::string puzzleDataPath(
const char *name)
const {
457 const std::filesystem::path shared_path = puzzle_asset_root /
"data" / name;
458 if (std::filesystem::exists(shared_path)) {
459 return shared_path.string();
461 return dataPath(name);
464 mxvk::VK_Sprite *loadPngSprite(
const char *name) {
return createSprite(dataPath(name)); }
466 mxvk::VK_Sprite *loadEffectSprite(
const char *name) {
return createSprite(dataPath(name),
"", dataPath(
"intro.frag.spv")); }
468 mxvk::VK_Sprite *makeSolidPixel(std::uint8_t r, std::uint8_t g, std::uint8_t b, std::uint8_t a) {
469 const std::array<std::uint8_t, 4> pixel{r, g, b, a};
476 background_intro = loadEffectSprite(
"intro.png");
477 background_menu = loadEffectSprite(
"start.png");
478 background_game =
createSprite(puzzleDataPath(
"gamebg.png"));
479 mxvk_logo =
createSprite(puzzleDataPath(
"mxvk_logo.png"));
480 cursor = loadPngSprite(
"cursor.png");
482 for (std::size_t i = 0; i < block_files.size(); ++i) {
483 blocks[i] = loadPngSprite(block_files[i]);
486 for (std::size_t i = 0; i < menu_files.size(); ++i) {
487 menu_items[i] = loadPngSprite(menu_files[i]);
490 panel = makeSolidPixel(0, 0, 0, 192);
491 overlay = makeSolidPixel(0, 0, 0, 128);
494 void setScreen(Screen next) {
496 SDL_StopTextInput(
window.get());
504 SDL_StartTextInput(
window.get());
509 intro_start_ms = SDL_GetTicks();
514 for (
auto &row : board) {
515 for (Cell &cell : row) {
523 lines_toward_speedup = 0;
526 fall_accumulator_ms = 0;
528 awaiting_name =
false;
530 waiting_for_spawn =
false;
533 piece.next_colors = randomColors();
537 std::array<int, piece_height> randomColors() {
538 std::uniform_int_distribution<int> dist(1, 9);
539 std::array<int, piece_height> colors{dist(rng), dist(rng), dist(rng)};
540 while (colors[0] == colors[1] && colors[1] == colors[2]) {
541 colors[1] = dist(rng);
546 bool canPlacePiece(
int x,
int y)
const {
547 if (x < 0 || x >= board_cols) {
552 const int row = y + i;
553 if (row < 0 || row >= board_rows) {
556 if (board[
static_cast<std::size_t
>(row)][
static_cast<std::size_t
>(x)].color != 0) {
563 bool movePiece(
int dx,
int dy) {
564 const int next_x = piece.x + dx;
565 const int next_y = piece.y + dy;
566 if (!canPlacePiece(next_x, next_y)) {
576 while (movePiece(0, 1)) {
578 lockPiece(SDL_GetTicks());
581 void rotatePieceColors(
bool forward) {
583 const int temp = piece.colors.back();
584 piece.colors[2] = piece.colors[1];
585 piece.colors[1] = piece.colors[0];
586 piece.colors[0] = temp;
588 const int temp = piece.colors.front();
589 piece.colors[0] = piece.colors[1];
590 piece.colors[1] = piece.colors[2];
591 piece.colors[2] = temp;
595 void handleControllerButton(Uint8 button) {
598 if (button == SDL_GAMEPAD_BUTTON_SOUTH || button == SDL_GAMEPAD_BUTTON_START) {
603 if (button == SDL_GAMEPAD_BUTTON_DPAD_UP) {
604 menu_selection = (menu_selection +
menu_item_count - 1) % menu_item_count;
605 }
else if (button == SDL_GAMEPAD_BUTTON_DPAD_DOWN) {
606 menu_selection = (menu_selection + 1) % menu_item_count;
607 }
else if (button == SDL_GAMEPAD_BUTTON_SOUTH || button == SDL_GAMEPAD_BUTTON_START) {
608 handleMenuSelection();
609 }
else if (button == SDL_GAMEPAD_BUTTON_EAST || button == SDL_GAMEPAD_BUTTON_BACK) {
614 if (button == SDL_GAMEPAD_BUTTON_BACK) {
616 }
else if (button == SDL_GAMEPAD_BUTTON_START) {
618 }
else if (paused || awaiting_name) {
620 }
else if (button == SDL_GAMEPAD_BUTTON_DPAD_LEFT) {
622 }
else if (button == SDL_GAMEPAD_BUTTON_DPAD_RIGHT) {
624 }
else if (button == SDL_GAMEPAD_BUTTON_DPAD_DOWN) {
625 if (!movePiece(0, 1)) {
626 lockPiece(SDL_GetTicks());
628 }
else if (button == SDL_GAMEPAD_BUTTON_DPAD_UP || button == SDL_GAMEPAD_BUTTON_SOUTH) {
629 rotatePieceColors(
true);
630 }
else if (button == SDL_GAMEPAD_BUTTON_EAST) {
631 rotatePieceColors(
false);
632 }
else if (button == SDL_GAMEPAD_BUTTON_NORTH) {
638 if (button == SDL_GAMEPAD_BUTTON_SOUTH || button == SDL_GAMEPAD_BUTTON_START) {
640 }
else if (button == SDL_GAMEPAD_BUTTON_EAST || button == SDL_GAMEPAD_BUTTON_BACK) {
642 SDL_StopTextInput(
window.get());
645 }
else if (button == SDL_GAMEPAD_BUTTON_SOUTH || button == SDL_GAMEPAD_BUTTON_START || button == SDL_GAMEPAD_BUTTON_EAST || button == SDL_GAMEPAD_BUTTON_BACK) {
650 if (button == SDL_GAMEPAD_BUTTON_SOUTH || button == SDL_GAMEPAD_BUTTON_START || button == SDL_GAMEPAD_BUTTON_EAST || button == SDL_GAMEPAD_BUTTON_BACK) {
655 if (button == SDL_GAMEPAD_BUTTON_SOUTH || button == SDL_GAMEPAD_BUTTON_START) {
657 }
else if (button == SDL_GAMEPAD_BUTTON_EAST || button == SDL_GAMEPAD_BUTTON_BACK) {
659 SDL_StopTextInput(
window.get());
666 void handleMenuSelection() {
667 switch (menu_selection) {
686 void handleMenuKey(SDL_Keycode key) {
687 if (key == SDLK_ESCAPE) {
692 if (key == SDLK_UP) {
693 menu_selection = (menu_selection +
menu_item_count - 1) % menu_item_count;
697 if (key == SDLK_DOWN) {
698 menu_selection = (menu_selection + 1) % menu_item_count;
702 if (!isConfirmKey(key)) {
706 handleMenuSelection();
709 void handleGameKey(SDL_Keycode key) {
710 if (key == SDLK_ESCAPE) {
715 if (key ==
'p' || key ==
'P') {
720 if (paused || awaiting_name) {
724 if (key == SDLK_LEFT) {
726 }
else if (key == SDLK_RIGHT) {
728 }
else if (key == SDLK_DOWN) {
729 if (!movePiece(0, 1)) {
730 lockPiece(SDL_GetTicks());
732 }
else if (key ==
'a' || key ==
'A' || key == SDLK_UP) {
733 rotatePieceColors(
true);
734 }
else if (key ==
's' || key ==
'S') {
735 rotatePieceColors(
false);
739 void handleNameKey(SDL_Keycode key) {
740 if (key == SDLK_ESCAPE) {
742 SDL_StopTextInput(
window.get());
747 if (key == SDLK_BACKSPACE) {
748 if (!player_name.empty()) {
749 player_name.pop_back();
754 if (key == SDLK_RETURN) {
759 void handleNameText(
const char *text) {
760 if (text ==
nullptr) {
764 while (*text !=
'\0' &&
static_cast<int>(player_name.size()) < max_name_length) {
765 const unsigned char ch =
static_cast<unsigned char>(*text++);
766 if (ch >= 32 && ch < 127) {
767 player_name.push_back(
static_cast<char>(ch));
774 if (player_name.empty()) {
775 player_name =
"Player";
777 high_scores.add(player_name, score);
781 SDL_StopTextInput(
window.get());
786 piece.colors = piece.next_colors;
787 piece.next_colors = randomColors();
791 if (!canPlacePiece(piece.x, piece.y)) {
796 bool boardHasFlashCells()
const {
797 for (
const auto &row : board) {
798 for (
const Cell &cell : row) {
799 if (cell.flash_until != 0U) {
807 bool updateFlashState(Uint64 now) {
808 if (!boardHasFlashCells()) {
812 bool expired_any =
false;
813 for (
auto &row : board) {
814 for (Cell &cell : row) {
815 if (cell.flash_until != 0U && now >= cell.flash_until) {
822 if (boardHasFlashCells()) {
831 if (!boardHasFlashCells() && waiting_for_spawn) {
832 waiting_for_spawn =
false;
839 void updateGame(Uint64 now) {
840 if (paused || awaiting_name) {
844 if (last_update_ms == 0U) {
845 last_update_ms = now;
849 if (updateFlashState(now)) {
850 last_update_ms = now;
854 if (waiting_for_spawn) {
855 waiting_for_spawn =
false;
857 last_update_ms = now;
861 const Uint64 delta = now - last_update_ms;
862 last_update_ms = now;
863 fall_accumulator_ms += delta;
865 while (fall_accumulator_ms >=
static_cast<Uint64
>(fall_delay_ms)) {
866 fall_accumulator_ms -=
static_cast<Uint64
>(fall_delay_ms);
867 if (!movePiece(0, 1)) {
874 bool resolveMatches(Uint64 now) {
875 std::array<std::array<bool, board_cols>,
board_rows> marked{};
876 int matches_found = 0;
878 auto mark_run = [&](
int x,
int y,
int dx,
int dy) {
879 const int color = board[
static_cast<std::size_t
>(y)][
static_cast<std::size_t
>(x)].color;
884 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) {
897 for (
int i = 0; i < length; ++i) {
898 marked[
static_cast<std::size_t
>(cy)][
static_cast<std::size_t
>(cx)] =
true;
906 const Cell &cell = board[
static_cast<std::size_t
>(y)][
static_cast<std::size_t
>(x)];
907 if (cell.color == 0 || cell.flash_until != 0U) {
911 if (x == 0 || board[
static_cast<std::size_t
>(y)][
static_cast<std::size_t
>(x - 1)].color != cell.color) {
912 mark_run(x, y, 1, 0);
914 if (y == 0 || board[
static_cast<std::size_t
>(y - 1)][
static_cast<std::size_t
>(x)].color != cell.color) {
915 mark_run(x, y, 0, 1);
917 if (x == 0 || y == 0 || board[
static_cast<std::size_t
>(y - 1)][
static_cast<std::size_t
>(x - 1)].color != cell.color) {
918 mark_run(x, y, 1, 1);
920 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) {
921 mark_run(x, y, -1, 1);
926 if (matches_found == 0) {
932 if (marked[
static_cast<std::size_t
>(y)][
static_cast<std::size_t
>(x)]) {
933 board[
static_cast<std::size_t
>(y)][
static_cast<std::size_t
>(x)].flash_until = now +
flash_time_ms;
939 lines += matches_found;
940 lines_toward_speedup += matches_found;
941 while (lines_toward_speedup >= lines_per_speedup) {
944 fall_delay_ms = std::max(140, 520 - speed_level * 40);
950 void applyGravity() {
953 for (
int y = board_rows - 1; y >= 0; --y) {
954 if (board[
static_cast<std::size_t
>(y)][
static_cast<std::size_t
>(x)].color != 0) {
955 if (write_row != y) {
956 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)];
957 board[
static_cast<std::size_t
>(y)][
static_cast<std::size_t
>(x)] = {};
963 for (
int y = write_row; y >= 0; --y) {
964 board[
static_cast<std::size_t
>(y)][
static_cast<std::size_t
>(x)] = {};
969 void lockPiece(Uint64 now) {
976 const int row = piece.y + i;
977 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);
980 if (resolveMatches(now)) {
981 waiting_for_spawn =
true;
988 void handleGameOver() {
989 if (high_scores.qualifies(score)) {
998 void drawSprite(mxvk::VK_Sprite *sprite,
int x,
int y,
int w,
int h) {
999 if (sprite !=
nullptr) {
1004 void drawCenteredText(
const std::string &text,
int y,
const SDL_Color &color,
const mxvk::Font &font) {
1008 printText(text, scaled(32, layout.scale_x), y, color, font);
1012 const int x = std::max(16, (layout.width - w) / 2);
1016 bool drawIntro(Uint64 now) {
1017 const float elapsed = intro_start_ms == 0U ? 0.0f :
static_cast<float>(now - intro_start_ms) / 1000.0f;
1018 background_intro->setShaderParams(elapsed, 0.0f, 0.0f, 1.0f);
1019 drawSprite(background_intro, 0, 0, layout.width, layout.height);
1021 if (intro_start_ms == 0U) {
1022 intro_start_ms = now;
1025 if (now - intro_start_ms > title_screen_time_ms) {
1034 const float elapsed =
static_cast<float>(SDL_GetTicks()) / 1000.0f;
1035 background_menu->setShaderParams(elapsed, 0.0f, 0.0f, 1.0f);
1036 drawSprite(background_menu, 0, 0, layout.width, layout.height);
1039 const int y = layout.menu_y + i * layout.menu_step;
1040 if (i == menu_selection) {
1041 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));
1043 drawSprite(menu_items[
static_cast<std::size_t
>(i)], layout.menu_x, y, layout.menu_w, layout.menu_h);
1047 void drawScores(
bool entering_name) {
1048 const float elapsed =
static_cast<float>(SDL_GetTicks()) / 1000.0f;
1049 background_menu->setShaderParams(elapsed, 0.0f, 0.0f, 1.0f);
1050 drawSprite(background_menu, 0, 0, layout.width, layout.height);
1051 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));
1053 drawCenteredText(
"High Scores", scaled(72, layout.scale_y), SDL_Color{255, 245, 200, 255}, title_font);
1055 const auto &entries = high_scores.list();
1056 const int start_y = scaled(140, layout.scale_y);
1057 const int step_y = scaled(30, layout.scale_y);
1058 for (std::size_t i = 0; i < entries.size(); ++i) {
1059 const std::string line = std::format(
"{:>2}. {:<16} {}", i + 1, entries[i].name, entries[i].score);
1060 printText(line, scaled(70, layout.scale_x), start_y +
static_cast<int>(i) * step_y, SDL_Color{255, 255, 255, 255}, ui_font);
1063 if (entering_name) {
1064 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);
1065 printText(
"Name:", scaled(70, layout.scale_x), scaled(490, layout.scale_y), SDL_Color{255, 245, 200, 255}, ui_font);
1066 printText(player_name +
"_", scaled(150, layout.scale_x), scaled(490, layout.scale_y), SDL_Color{255, 255, 255, 255}, ui_font);
1068 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);
1072 void drawCredits() {
1073 const float elapsed =
static_cast<float>(SDL_GetTicks()) / 1000.0f;
1074 background_menu->setShaderParams(elapsed, 0.0f, 0.0f, 1.0f);
1075 drawSprite(background_menu, 0, 0, layout.width, layout.height);
1076 drawSprite(overlay, scaled(30, layout.scale_x), scaled(85, layout.scale_y), layout.width - scaled(60, layout.scale_x), scaled(260, layout.scale_y));
1077 const int logo_w = layout.width / 2;
1078 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())));
1079 const int logo_x = (layout.width - logo_w) / 2;
1080 const int logo_y = (layout.height - logo_h) / 2;
1081 drawSprite(mxvk_logo, logo_x, logo_y, logo_w, logo_h);
1082 drawCenteredText(
"Credits", scaled(96, layout.scale_y), SDL_Color{255, 245, 200, 255}, title_font);
1083 printText(
"Original game: MasterPiece.SDL", scaled(70, layout.scale_x), scaled(180, layout.scale_y), SDL_Color{255, 255, 255, 255}, ui_font);
1084 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);
1085 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);
1088 void drawGame(Uint64 now) {
1089 const float scaleX =
static_cast<float>(layout.width) /
static_cast<float>(game_base_width);
1090 const float scaleY =
static_cast<float>(layout.height) /
static_cast<float>(game_base_height);
1091 drawSprite(background_game, 0, 0, layout.width, layout.height);
1092 drawBoard(now, scaleX, scaleY);
1093 drawNextPiece(scaleX, scaleY);
1094 drawHud(scaleX, scaleY);
1097 const char *pausedText =
"PAUSED - Press P to Continue";
1098 int pausedWidth = 0;
1099 int pausedHeight = 0;
1101 pausedWidth =
static_cast<int>(std::strlen(pausedText)) * 16;
1103 printText(pausedText, layout.width / 2 - pausedWidth / 2, layout.height / 2, SDL_Color{255, 255, 0, 255}, title_font);
1107 void drawBoard(Uint64 now,
float scaleX,
float scaleY) {
1110 const Cell &cell = board[
static_cast<std::size_t
>(j)][
static_cast<std::size_t
>(i)];
1111 if (cell.color == 0) {
1115 const int sprite_index = cell.flash_until != 0U ? flashingSpriteIndex(i, j, now) : std::clamp(cell.color, 0, 9);
1119 blocks[
static_cast<std::size_t
>(sprite_index)]->drawSpriteRect(
static_cast<int>(
static_cast<float>(x) * scaleX),
static_cast<int>(
static_cast<float>(y) * scaleY) + 10,
static_cast<int>(
static_cast<float>(game_block_width) * scaleX),
static_cast<int>(
static_cast<float>(game_block_height) * scaleY));
1128 const int row = piece.y + i;
1129 if (row < 0 || row >= board_rows || piece.x < 0 || piece.x >= board_cols) {
1135 blocks[
static_cast<std::size_t
>(std::clamp(piece.colors[
static_cast<std::size_t
>(i)], 0, 9))]->drawSpriteRect(
static_cast<int>(
static_cast<float>(x) * scaleX),
static_cast<int>(
static_cast<float>(y) * scaleY) + 10,
static_cast<int>(
static_cast<float>(game_block_width) * scaleX),
static_cast<int>(
static_cast<float>(game_block_height) * scaleY));
1139 void drawNextPiece(
float scaleX,
float scaleY) {
1144 const int sprite_index = std::clamp(piece.next_colors[
static_cast<std::size_t
>(i)], 0, 9);
1145 blocks[
static_cast<std::size_t
>(sprite_index)]->drawSpriteRect(
static_cast<int>(
static_cast<float>(bx) * scaleX),
static_cast<int>(
static_cast<float>(by + i * (game_block_height + game_block_spacing)) * scaleY),
static_cast<int>(
static_cast<float>(game_block_width) * scaleX),
static_cast<int>(
static_cast<float>(game_block_height) * scaleY));
1149 void drawHud(
float scaleX,
float scaleY) {
1150 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);
1151 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);
1154 void handleControllerAxis(Sint16 lx, Sint16 ly) {
1155 const Uint32 now = SDL_GetTicks();
1158 if (ly < -joystick_dead_zone) {
1159 if (now - joy_repeat_up_ms > joy_repeat_delay_ms) {
1160 menu_selection = (menu_selection +
menu_item_count - 1) % menu_item_count;
1161 joy_repeat_up_ms = now;
1164 joy_repeat_up_ms = 0U;
1167 if (ly > joystick_dead_zone) {
1168 if (now - joy_repeat_down_ms > joy_repeat_delay_ms) {
1169 menu_selection = (menu_selection + 1) % menu_item_count;
1170 joy_repeat_down_ms = now;
1173 joy_repeat_down_ms = 0U;
1178 if (screen !=
Screen::Game || paused || awaiting_name) {
1182 if (lx < -joystick_dead_zone) {
1183 if (now - joy_repeat_left_ms > joy_repeat_delay_ms) {
1185 joy_repeat_left_ms = now;
1188 joy_repeat_left_ms = 0U;
1191 if (lx > joystick_dead_zone) {
1192 if (now - joy_repeat_right_ms > joy_repeat_delay_ms) {
1194 joy_repeat_right_ms = now;
1197 joy_repeat_right_ms = 0U;
1200 if (ly > joystick_dead_zone) {
1201 if (now - joy_repeat_down_ms > joy_repeat_delay_ms) {
1202 if (!movePiece(0, 1)) {
1205 joy_repeat_down_ms = now;
1208 joy_repeat_down_ms = 0U;
1211 if (lx == 0 && ly == 0) {
1212 joy_repeat_up_ms = 0U;
1216 void pollController([[maybe_unused]] Uint64 now) {
1217 if (gamepad ==
nullptr) {
1221 const Sint16 lx = SDL_GetGamepadAxis(gamepad, SDL_GAMEPAD_AXIS_LEFTX);
1222 const Sint16 ly = SDL_GetGamepadAxis(gamepad, SDL_GAMEPAD_AXIS_LEFTY);
1223 handleControllerAxis(lx, ly);
1226 bool openGamepad(SDL_JoystickID
id) {
1227 if (gamepad !=
nullptr && gamepadId ==
id) {
1232 gamepad = SDL_OpenGamepad(
id);
1233 if (gamepad ==
nullptr) {
1241 void tryOpenFirstGamepad() {
1242 if (gamepad !=
nullptr) {
1247 SDL_JoystickID *ids = SDL_GetGamepads(&count);
1248 if (ids ==
nullptr || count <= 0) {
1249 if (ids !=
nullptr) {
1255 openGamepad(ids[0]);
1259 void closeGamepad() {
1260 if (gamepad !=
nullptr) {
1261 SDL_CloseGamepad(gamepad);
1274 if (!explicit_resolution) {
1282 std::cerr << std::format(
"mxvk: Exception: {}\n", e.
text());
1283 return EXIT_FAILURE;
1285 std::cerr << std::format(
"mxvk: Argument Exception: {}\n", e.
text());
1286 return EXIT_FAILURE;
1289 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