12#ifndef mutatris_ASSET_DIR
13#define mutatris_ASSET_DIR "."
29 dataRoot(assetRoot +
"/data"),
30 shaderRoot(assetRoot +
"/shaders"),
31 fontPath(dataRoot +
"/font.ttf"),
32 crtEnabled(enableCrt) {
41 syncControllerConnection();
43 startupStartTick = SDL_GetTicks();
44 logMutatris(std::format(
"Mutatris ready. {} shader effect(s) available.", effectShaders.size()));
48 if (e.type == SDL_EVENT_GAMEPAD_ADDED) {
49 if (!controller.active()) {
50 controller.connectEvent(e);
52 syncControllerConnection();
55 if (e.type == SDL_EVENT_GAMEPAD_REMOVED) {
56 controller.connectEvent(e);
57 syncControllerConnection();
61 const bool wasConsoleVisible =
console.isVisible();
62 const bool consoleToggle = e.type == SDL_EVENT_KEY_DOWN && e.key.key == SDLK_F3;
65 logMutatris(
console.isVisible() ?
"Console opened." :
"Console closed.");
68 if (wasConsoleVisible) {
71 if (e.type == SDL_EVENT_KEY_DOWN && e.key.key == SDLK_ESCAPE) {
79 logMutatris(
"Exit requested.");
84 if (e.type == SDL_EVENT_KEY_DOWN && e.key.key == SDLK_F8 && !e.key.repeat) {
85 crtEnabled = !crtEnabled;
87 logMutatris(std::string(
"CRT effect ") + (crtEnabled ?
"enabled." :
"disabled."));
90 if (e.type == SDL_EVENT_KEY_DOWN && !e.key.repeat) {
92 }
else if (e.type == SDL_EVENT_MOUSE_BUTTON_DOWN || e.type == SDL_EVENT_FINGER_DOWN) {
94 }
else if (e.type == SDL_EVENT_GAMEPAD_BUTTON_DOWN) {
95 handleGamepad(e.gbutton.button);
101 width = extent.width > 0U ?
static_cast<int>(extent.width) :
DESIGN_WIDTH;
102 height = extent.height > 0U ?
static_cast<int>(extent.height) :
DESIGN_HEIGHT;
105 ensureMusicPlaying();
126 void MutatrisWindow::configureConsole() {
127 console.attach(*
this, fontPath, CONSOLE_DESIGN_FONT_SIZE);
128 console.setSpriteYOriginTopLeft(
true);
129 console.setPrompt(
"mutatris> ");
130 console.printLine(
"Press F3 to open/close the console.");
131 console.printLine(
"Type 'help' for commands.");
132 console.setCommandCallback([
this](
mxvk::VK_Window &,
const std::vector<std::string> &args, std::ostream &out) {
137 if (args[0] ==
"help") {
139 <<
" help Show this help message\n"
140 <<
" clear Clear the console output\n"
141 <<
" echo <text> Print text to the console\n"
142 <<
" switch_shader Pick a random background and shader\n"
143 <<
" about Show app information\n"
144 <<
" quit | exit Close Mutatris";
148 if (args[0] ==
"echo") {
149 for (std::size_t i = 1; i < args.size(); ++i) {
158 if (args[0] ==
"switch_shader") {
159 const std::string shaderName = switchBackgroundShader(
true);
160 if (shaderName.empty()) {
161 out <<
"No shader effects are available.";
163 logMutatris(std::format(
"Console command switch_shader selected {} with shader {}", backgroundName(backgroundIndex), shaderName));
164 out << std::format(
"Switched to {} with shader {}", backgroundName(backgroundIndex), shaderName);
169 if (args[0] ==
"about") {
170 out <<
"Mutatris: rotating-grid puzzle game built with MXVK.";
174 if (args[0] ==
"quit" || args[0] ==
"exit") {
175 out <<
"Closing Mutatris...";
176 logMutatris(
"Exit requested from console.");
185 void MutatrisWindow::logMutatris(
const std::string &message) {
186 std::cout << std::format(
"mutatris: {}\n", message);
187 console.printLine(message, SDL_Color{180, 220, 255, 255});
190 const char *MutatrisWindow::screenName(
Screen value)
const {
206 const char *MutatrisWindow::focusName(
int value)
const {
221 void MutatrisWindow::setScreen(
Screen nextScreen,
const std::string &reason) {
222 if (screen == nextScreen) {
225 const Screen previousScreen = screen;
227 logMutatris(std::format(
"Screen changed {} -> {} ({})", screenName(previousScreen), screenName(nextScreen), reason));
230 void MutatrisWindow::loadSprites() {
231 const std::string backgroundVertexShader = shaderRoot +
"/background.vert.spv";
232 const std::string backgroundShader = shaderRoot +
"/background.frag.spv";
233 const std::string fadeShader = shaderRoot +
"/fade.frag.spv";
235 for (std::size_t i = 0; i < backgrounds.size(); ++i) {
236 backgrounds[i] =
createSprite(dataRoot +
"/blocks" + (i == 0 ? std::string(
"") : std::to_string(i)) +
".png", backgroundVertexShader, backgroundShader);
238 intro =
createSprite(dataRoot +
"/intro.png", backgroundVertexShader, fadeShader);
240 lostLogo =
createSprite(dataRoot +
"/lostlogo.png", backgroundVertexShader, fadeShader);
241 jbLogo =
createSprite(dataRoot +
"/jblogo.png", backgroundVertexShader, fadeShader);
245 const std::array<std::string, 8> blockFiles{{
255 for (std::size_t i = 0; i < blockFiles.size(); ++i) {
256 blocks[i] =
createSprite(dataRoot +
"/" + blockFiles[i]);
260 void MutatrisWindow::loadEffectShaders() {
261 effectShaders.clear();
262 const std::filesystem::path effectDir = std::filesystem::path(shaderRoot) /
"effects";
263 if (!std::filesystem::exists(effectDir)) {
266 for (
const std::filesystem::directory_entry &entry : std::filesystem::directory_iterator(effectDir)) {
267 if (entry.is_regular_file() && entry.path().extension() ==
".spv") {
268 effectShaders.push_back(entry.path().string());
271 std::sort(effectShaders.begin(), effectShaders.end());
272 std::cout << std::format(
"mutatris: loaded {} shader effect(s)\n", effectShaders.size());
275 void MutatrisWindow::loadSoundEffects() {
276#if defined(MXVK_WITH_MIXER) || defined(WITH_MIXER)
277 soundEffects = std::make_unique<mxvk::VK_Mixer>();
278 musicTrack = soundEffects->loadMusic(dataRoot +
"/music.ogg");
279 lineSound = soundEffects->loadWav(dataRoot +
"/line.wav");
280 openSound = soundEffects->loadWav(dataRoot +
"/open.wav");
281 logMutatris(
"Loaded original Mutatris sound effects.");
282 ensureMusicPlaying();
286 void MutatrisWindow::playSound(
int soundId) {
287#if defined(MXVK_WITH_MIXER) || defined(WITH_MIXER)
288 if (soundEffects ==
nullptr || soundId < 0) {
291 soundEffects->playWav(soundId);
297 void MutatrisWindow::ensureMusicPlaying() {
298#if defined(MXVK_WITH_MIXER) || defined(WITH_MIXER)
299 if (soundEffects ==
nullptr || musicTrack < 0 || soundEffects->isMusicPlaying(musicTrack)) {
302 if (soundEffects->playMusic(musicTrack, -1) != 0) {
303 throw mxvk::Exception(
"Could not start Mutatris background music");
308 void MutatrisWindow::handleKey(SDL_Keycode key) {
310 if ((key == SDLK_RETURN || key == SDLK_KP_ENTER || key == SDLK_SPACE) && isStartupTitleFullyVisible()) {
317 if (screen ==
Screen::Title && (key == SDLK_RETURN || key == SDLK_KP_ENTER || key == SDLK_SPACE)) {
322 if (key == SDLK_LEFT && difficulty > 0) {
324 logMutatris(std::format(
"Difficulty changed to {}", difficulty));
325 }
else if (key == SDLK_RIGHT && difficulty < 2) {
327 logMutatris(std::format(
"Difficulty changed to {}", difficulty));
328 }
else if (key == SDLK_RETURN || key == SDLK_KP_ENTER || key == SDLK_SPACE) {
333 if (screen ==
Screen::GameOver && (key == SDLK_SPACE || key == SDLK_RETURN || key == SDLK_KP_ENTER)) {
341 game->grid[focus].gamePiece.shiftColors();
344 if (key == SDLK_A || key == SDLK_SPACE) {
345 game->grid[focus].gamePiece.shiftDirection();
349 if (game->grid[focus].gamePiece.drop()) {
352 }
else if (activePiecePlacementBlocked()) {
353 triggerGameOver(
"active piece placement blocked");
359 void MutatrisWindow::handleGamepad(Uint8 button) {
360 if (button == SDL_GAMEPAD_BUTTON_BACK) {
361 logMutatris(
"Exit requested.");
366 if (button == SDL_GAMEPAD_BUTTON_SOUTH || button == SDL_GAMEPAD_BUTTON_START) {
368 }
else if (screen ==
Screen::Difficulty && button == SDL_GAMEPAD_BUTTON_DPAD_LEFT && difficulty > 0) {
370 logMutatris(std::format(
"Difficulty changed to {}", difficulty));
371 }
else if (screen ==
Screen::Difficulty && button == SDL_GAMEPAD_BUTTON_DPAD_RIGHT && difficulty < 2) {
373 logMutatris(std::format(
"Difficulty changed to {}", difficulty));
377 if (game ==
nullptr) {
380 if (button == SDL_GAMEPAD_BUTTON_SOUTH) {
381 game->grid[focus].gamePiece.shiftColors();
382 }
else if (button == SDL_GAMEPAD_BUTTON_EAST) {
383 game->grid[focus].gamePiece.shiftDirection();
384 }
else if (button == SDL_GAMEPAD_BUTTON_NORTH) {
385 if (game->grid[focus].gamePiece.drop()) {
388 }
else if (activePiecePlacementBlocked()) {
389 triggerGameOver(
"active piece placement blocked");
391 }
else if (button == SDL_GAMEPAD_BUTTON_DPAD_LEFT) {
392 handleGamepadDpad(button);
393 }
else if (button == SDL_GAMEPAD_BUTTON_DPAD_RIGHT) {
394 handleGamepadDpad(button);
395 }
else if (button == SDL_GAMEPAD_BUTTON_DPAD_UP) {
396 handleGamepadDpad(button);
397 }
else if (button == SDL_GAMEPAD_BUTTON_DPAD_DOWN) {
398 handleGamepadDpad(button);
402 void MutatrisWindow::handleGamepadDpad(Uint8 button) {
403 if (game ==
nullptr) {
406 const Uint32 now = SDL_GetTicks();
411 Piece &piece = game->grid[focus].gamePiece;
414 if (button == SDL_GAMEPAD_BUTTON_DPAD_LEFT) {
416 }
else if (button == SDL_GAMEPAD_BUTTON_DPAD_RIGHT) {
418 }
else if (button == SDL_GAMEPAD_BUTTON_DPAD_DOWN) {
419 softDropActivePiece();
423 }
else if (focus == 1) {
424 if (button == SDL_GAMEPAD_BUTTON_DPAD_UP) {
426 }
else if (button == SDL_GAMEPAD_BUTTON_DPAD_DOWN) {
428 }
else if (button == SDL_GAMEPAD_BUTTON_DPAD_RIGHT) {
429 softDropActivePiece();
433 }
else if (focus == 2) {
434 if (button == SDL_GAMEPAD_BUTTON_DPAD_LEFT) {
436 }
else if (button == SDL_GAMEPAD_BUTTON_DPAD_RIGHT) {
438 }
else if (button == SDL_GAMEPAD_BUTTON_DPAD_UP) {
439 softDropActivePiece();
443 }
else if (focus == 3) {
444 if (button == SDL_GAMEPAD_BUTTON_DPAD_DOWN) {
446 }
else if (button == SDL_GAMEPAD_BUTTON_DPAD_UP) {
448 }
else if (button == SDL_GAMEPAD_BUTTON_DPAD_LEFT) {
449 softDropActivePiece();
460 void MutatrisWindow::handleConfirm() {
462 if (isStartupTitleFullyVisible()) {
476 bool MutatrisWindow::isStartupTitleFullyVisible()
const {
477 const Uint32 elapsed = SDL_GetTicks() - startupStartTick;
479 constexpr Uint32 FIRST_FADE_OUT_END = FIRST_FADE_IN_END +
STARTUP_FADE_MS;
480 constexpr Uint32 SECOND_FADE_IN_END = FIRST_FADE_OUT_END +
STARTUP_FADE_MS;
481 constexpr Uint32 SECOND_HOLD_END = SECOND_FADE_IN_END +
STARTUP_HOLD_MS;
482 constexpr Uint32 SECOND_FADE_OUT_END = SECOND_HOLD_END +
STARTUP_FADE_MS;
484 return elapsed >= SECOND_FADE_OUT_END;
487 void MutatrisWindow::pollKeyboardInput() {
488 const bool *keyboardState = SDL_GetKeyboardState(
nullptr);
489 SDL_Keycode key = SDLK_UNKNOWN;
490 if (keyboardState[SDL_SCANCODE_LEFT]) {
492 }
else if (keyboardState[SDL_SCANCODE_RIGHT]) {
494 }
else if (keyboardState[SDL_SCANCODE_UP]) {
496 }
else if (keyboardState[SDL_SCANCODE_DOWN]) {
500 if (key == SDLK_UNKNOWN) {
501 lastKeyboardInputTick = 0;
505 const Uint32 now = SDL_GetTicks();
506 if (lastKeyboardInputTick != 0U && now - lastKeyboardInputTick <
KEY_REPEAT_MS) {
509 lastKeyboardInputTick = now;
510 handleDirectionalKey(key);
513 void MutatrisWindow::handleDirectionalKey(SDL_Keycode key) {
514 if (game ==
nullptr) {
517 Piece &piece = game->grid[focus].gamePiece;
519 if (key == SDLK_LEFT) {
521 }
else if (key == SDLK_RIGHT) {
523 }
else if (key == SDLK_UP) {
525 }
else if (key == SDLK_DOWN) {
526 if (piece.moveDown()) {
529 }
else if (activePiecePlacementBlocked()) {
530 triggerGameOver(
"active piece placement blocked");
533 }
else if (focus == 1) {
534 if (key == SDLK_DOWN) {
536 }
else if (key == SDLK_UP) {
538 }
else if (key == SDLK_LEFT) {
540 }
else if (key == SDLK_RIGHT) {
541 if (piece.moveDown()) {
544 }
else if (activePiecePlacementBlocked()) {
545 triggerGameOver(
"active piece placement blocked");
548 }
else if (focus == 2) {
549 if (key == SDLK_LEFT) {
551 }
else if (key == SDLK_RIGHT) {
553 }
else if (key == SDLK_DOWN) {
555 }
else if (key == SDLK_UP) {
556 if (piece.moveDown()) {
559 }
else if (activePiecePlacementBlocked()) {
560 triggerGameOver(
"active piece placement blocked");
563 }
else if (focus == 3) {
564 if (key == SDLK_DOWN) {
566 }
else if (key == SDLK_UP) {
568 }
else if (key == SDLK_RIGHT) {
570 }
else if (key == SDLK_LEFT) {
571 if (piece.moveDown()) {
574 }
else if (activePiecePlacementBlocked()) {
575 triggerGameOver(
"active piece placement blocked");
581 void MutatrisWindow::softDropActivePiece() {
582 if (game ==
nullptr) {
585 if (game->grid[focus].gamePiece.moveDown()) {
588 }
else if (activePiecePlacementBlocked()) {
589 triggerGameOver(
"active piece placement blocked");
593 bool MutatrisWindow::activePiecePlacementBlocked()
const {
594 if (game ==
nullptr) {
597 const Piece &piece = game->grid[focus].gamePiece;
598 return !piece.checkLocation(piece.getX(), piece.getY());
601 void MutatrisWindow::triggerGameOver(
const std::string &reason) {
602 if (game ==
nullptr) {
605 finalScore = game->score;
606 finalClears = game->clears;
608 logMutatris(std::format(
"Game over. score={} clears={}", finalScore, finalClears));
611 bool MutatrisWindow::openController() {
613 if (controller.open(index)) {
614 logMutatris(
"Controller connected: " + controller.name());
621 void MutatrisWindow::syncControllerConnection() {
622 if (!controller.active()) {
627 void MutatrisWindow::processGrid() {
628 if (game ==
nullptr) {
631 const int previousScore = game->score;
632 const int previousClears = game->clears;
633 const int previousLevel = game->level;
634 const unsigned int previousTimeout = game->timeout;
635 for (
int i = 0; i < 4; ++i) {
638 if (game->score != previousScore || game->clears != previousClears || game->level != previousLevel || game->timeout != previousTimeout) {
639 logMutatris(std::format(
"Grid processed. score {} -> {}, clears {} -> {}, level {} -> {}, timeout {} -> {}",
651 void MutatrisWindow::advanceFocus() {
652 const int previousFocus = focus;
655 }
else if (focus == 3) {
657 }
else if (focus == 2) {
662 lastDropTick = SDL_GetTicks();
663 logMutatris(std::format(
"Focus advanced {} -> {}.", focusName(previousFocus), focusName(focus)));
666 void MutatrisWindow::startGame() {
667 game = std::make_unique<PuzzleGame>(difficulty, [
this]() {
668 playSound(lineSound);
671 lastDropTick = SDL_GetTicks();
672 lastKeyboardInputTick = 0;
673 lastClearAnimationTick = lastDropTick;
676 backgroundIndex = -1;
678 logMutatris(std::format(
"New game started. difficulty={} timeout={}", difficulty, game->timeout));
681 void MutatrisWindow::drawStartup() {
682 const Uint32 now = SDL_GetTicks();
683 const Uint32 elapsed = now - startupStartTick;
685 constexpr Uint32 FIRST_FADE_OUT_END = FIRST_FADE_IN_END +
STARTUP_FADE_MS;
686 constexpr Uint32 SECOND_FADE_IN_END = FIRST_FADE_OUT_END +
STARTUP_FADE_MS;
687 constexpr Uint32 SECOND_HOLD_END = SECOND_FADE_IN_END +
STARTUP_HOLD_MS;
688 constexpr Uint32 SECOND_FADE_OUT_END = SECOND_HOLD_END +
STARTUP_FADE_MS;
689 constexpr Uint32 TITLE_FADE_IN_END = SECOND_FADE_OUT_END +
STARTUP_FADE_MS;
691 if (elapsed < FIRST_FADE_IN_END) {
692 drawStartupLogo(lostLogo,
static_cast<float>(elapsed) /
static_cast<float>(
STARTUP_FADE_MS));
693 }
else if (elapsed < FIRST_FADE_OUT_END) {
694 const Uint32 phaseElapsed = elapsed - FIRST_FADE_IN_END;
695 drawStartupLogo(lostLogo, 1.0f - (
static_cast<float>(phaseElapsed) /
static_cast<float>(
STARTUP_FADE_MS)));
696 }
else if (elapsed < SECOND_FADE_IN_END) {
697 const Uint32 phaseElapsed = elapsed - FIRST_FADE_OUT_END;
698 drawStartupLogo(jbLogo,
static_cast<float>(phaseElapsed) /
static_cast<float>(
STARTUP_FADE_MS));
699 }
else if (elapsed < SECOND_HOLD_END) {
700 drawStartupLogo(jbLogo, 1.0f);
701 }
else if (elapsed < SECOND_FADE_OUT_END) {
702 const Uint32 phaseElapsed = elapsed - SECOND_HOLD_END;
703 const float transitionAlpha =
static_cast<float>(phaseElapsed) /
static_cast<float>(
STARTUP_FADE_MS);
704 const float titleAlpha = std::clamp((transitionAlpha - 0.78f) / 0.22f, 0.0f, 1.0f);
705 drawTitleWithAlpha(titleAlpha);
706 drawStartupLogo(jbLogo, 1.0f - transitionAlpha);
707 }
else if (elapsed < TITLE_FADE_IN_END) {
708 drawTitleWithAlpha(1.0f);
714 void MutatrisWindow::drawStartupLogo(mxvk::VK_Sprite *sprite,
float alphaValue) {
715 if (sprite ==
nullptr) {
718 sprite->
setShaderParams(std::clamp(alphaValue, 0.0f, 1.0f), 0.0f, 0.0f, 0.0f);
722 void MutatrisWindow::drawTitle() {
723 drawTitleWithAlpha(1.0f);
726 void MutatrisWindow::drawTitleWithAlpha(
float alphaValue) {
728 const Uint8 alphaByte =
static_cast<Uint8
>(std::clamp(
static_cast<int>(std::lround(alphaValue * 255.0f)), 0, 255));
729 if (intro !=
nullptr) {
730 intro->setShaderParams(std::clamp(alphaValue, 0.0f, 1.0f), 0.0f, 0.0f, 0.0f);
731 intro->drawSpriteRect(0, 0, width, height);
733 printScaledText(
"[Press Enter to Play]", 25, 25, {255, 255, 255, alphaByte}, titleFont);
734 printScaledText(
"Mutatris", 50, 250, {255, 255, 255, alphaByte}, titleLargeFont);
735 printScaledText(
"lostsidedead.biz", 780, 670, {80, 120, 255, alphaByte}, titleFont);
738 void MutatrisWindow::drawDifficulty() {
740 if (start !=
nullptr) {
741 start->drawSpriteRect(0, 0, width, height);
743 printScaledText(
"Mutatris", 560, 325, {255, 255, 255, 255}, startMediumFont);
744 drawTextCenteredInRect(
750 {255, 255, 255, 255},
752 const std::array<std::string, 3> labels{{
"Easy",
"Medium",
"Hard"}};
753 for (
int i = 0; i < static_cast<int>(labels.size()); ++i) {
754 const SDL_Color color = i == difficulty ? SDL_Color{255, 230, 64, 255} : SDL_Color{255, 255, 255, 255};
755 printScaledText(labels[
static_cast<std::size_t
>(i)], 430 + i * 180, 475, color, difficultyFont);
759 void MutatrisWindow::updatePlaying() {
760 if (game ==
nullptr) {
768 updateBackgroundShaderForLevel();
769 mxvk::VK_Sprite *background = currentBackground();
770 if (background !=
nullptr) {
771 background->
setShaderParams(
static_cast<float>(SDL_GetTicks()) / 1000.0f, 0.0f, 0.0f, 0.0f);
774 twistClearedBlocks();
779 if (!console.isVisible()) {
780 printScaledText(
"Level: " + std::to_string(game->level + 1) +
" Timeout: " + std::to_string(game->timeout), 25, 25, {255, 255, 255, 255});
781 printScaledText(
"Score: " + std::to_string(game->score), 25, 55, {255, 255, 255, 255});
782 printScaledText(
"Direction: " + std::to_string(focus), 25, 85, {255, 255, 255, 255});
783 printScaledText(
"Arrows move W shift A rotate S drop", 25, 670, {220, 220, 220, 255});
786 const Uint32 now = SDL_GetTicks();
787 if (now - lastDropTick >= game->timeout) {
788 if (game->grid[focus].canMoveDown()) {
789 if (game->grid[focus].gamePiece.moveDown()) {
790 logMutatris(std::format(
"Timer drop advanced piece on {} grid.", focusName(focus)));
793 }
else if (activePiecePlacementBlocked()) {
794 triggerGameOver(
"active piece placement blocked");
798 triggerGameOver(
"active grid blocked");
803 void MutatrisWindow::updateBackgroundShaderForLevel() {
804 if (game ==
nullptr || game->level == shaderLevel) {
807 shaderLevel = game->level;
808 const std::string shaderName = switchBackgroundShader(
false);
809 if (!shaderName.empty()) {
810 logMutatris(std::format(
"Level {} selected {} with shader {}", game->level + 1, backgroundName(backgroundIndex), shaderName));
814 std::string MutatrisWindow::switchBackgroundShader(
bool force) {
815 selectRandomBackground();
816 if (effectShaders.empty()) {
819 std::uniform_int_distribution<int> shaderDistribution(0,
static_cast<int>(effectShaders.size()) - 1);
820 int nextShaderIndex = shaderDistribution(shaderRandom);
821 if (effectShaders.size() > 1U) {
822 while (nextShaderIndex == shaderIndex) {
823 nextShaderIndex = shaderDistribution(shaderRandom);
826 shaderIndex = nextShaderIndex;
828 const int targetLevel = game !=
nullptr ? game->level : 0;
829 mxvk::VK_Sprite *background = currentBackground();
830 if (background !=
nullptr) {
834 shaderLevel = targetLevel;
836 return std::filesystem::path(effectShaders[
static_cast<std::size_t
>(shaderIndex)]).filename().string();
839 void MutatrisWindow::selectRandomBackground() {
840 std::uniform_int_distribution<int> backgroundDistribution(0,
static_cast<int>(backgrounds.size()) - 1);
841 int nextBackgroundIndex = backgroundDistribution(shaderRandom);
842 if (backgrounds.size() > 1U) {
843 while (nextBackgroundIndex == backgroundIndex) {
844 nextBackgroundIndex = backgroundDistribution(shaderRandom);
847 backgroundIndex = nextBackgroundIndex;
850 mxvk::VK_Sprite *MutatrisWindow::currentBackground() {
851 if (backgroundIndex < 0) {
852 selectRandomBackground();
854 return backgrounds[
static_cast<std::size_t
>(std::clamp(backgroundIndex, 0,
static_cast<int>(backgrounds.size()) - 1))];
857 std::string MutatrisWindow::backgroundName(
int index) {
861 return std::format(
"blocks{}.png", index);
864 void MutatrisWindow::drawGameOver() {
865 mxvk::VK_Sprite *sprite = finalScore >= 200 ? highScore : gameOver;
866 if (sprite !=
nullptr) {
869 printScaledText((finalScore >= 200 ?
"High Score: " :
"Game Over Score: ") + std::to_string(finalScore) +
" Clears: " + std::to_string(finalClears), 25, 25, {255, 255, 255, 255});
870 printScaledText(
"[ Press Space ]", 25, finalScore >= 200 ? 400 : 100, {255, 255, 255, 255});
873 void MutatrisWindow::twistClearedBlocks() {
874 const Uint32 now = SDL_GetTicks();
875 const Uint32 elapsed = lastClearAnimationTick == 0U ? 0U : now - lastClearAnimationTick;
876 lastClearAnimationTick = now;
877 for (GameGrid &focusGrid : game->grid) {
878 for (
int y = 0; y < focusGrid.height(); ++y) {
879 for (
int x = 0; x < focusGrid.width(); ++x) {
880 Block *block = focusGrid.at(x, y);
881 if (block !=
nullptr && block->color < 0) {
882 block->clearElapsedMs += elapsed;
885 block->clearElapsedMs = 0;
888 block->color = -
static_cast<int>(frame + 1U);
896 void MutatrisWindow::drawGrid(
int gridIndex) {
897 const GameGrid &focusGrid = game->grid[
static_cast<std::size_t
>(gridIndex)];
898 const BoardLayout layout = boardLayout(gridIndex);
899 drawGridFrame(layout, gridIndex == focus);
900 for (
int y = 0; y < focusGrid.height(); ++y) {
901 for (
int x = 0; x < focusGrid.width(); ++x) {
902 const Block *block = focusGrid.at(x, y);
903 if (block !=
nullptr) {
904 drawCell(layout, x, y, block->color);
908 if (gridIndex == focus) {
909 const Piece &piece = focusGrid.gamePiece;
910 for (
int i = 0; i < 3; ++i) {
911 int x = piece.getX();
912 int y = piece.getY();
913 if (piece.getDirection() == 0) {
915 }
else if (piece.getDirection() == 1) {
917 }
else if (piece.getDirection() == 2) {
919 }
else if (piece.getDirection() == 3) {
922 const Block *block = piece.at(i);
923 if (block !=
nullptr) {
924 drawCell(layout, x, y, block->color);
930 MutatrisWindow::BoardLayout MutatrisWindow::boardLayout(
int gridIndex)
const {
931 if (gridIndex == 0) {
934 if (gridIndex == 1) {
937 if (gridIndex == 2) {
943 void MutatrisWindow::drawGridFrame(
const MutatrisWindow::BoardLayout &layout,
bool selected) {
944 const bool sideGrid = layout.gridIndex == 1 || layout.gridIndex == 3;
948 }
else if (layout.gridIndex == 2) {
954 const int thickness = selected ? 4 : 2;
955 mxvk::VK_Sprite *pixel = blocks[0];
956 const auto drawHorizontal = [&](
int x,
int y,
int w) {
957 pixel->
drawSpriteRect(scaleX(x), scaleY(y), scaleX(w), scaleY(thickness));
959 const auto drawVertical = [&](
int x,
int y,
int h) {
960 pixel->
drawSpriteRect(scaleX(x), scaleY(y), scaleX(thickness), scaleY(h));
963 if (layout.gridIndex == 0) {
964 drawHorizontal(layout.x - thickness, layout.y - thickness, boardW + thickness * 2);
965 drawVertical(layout.x - thickness, layout.y,
SIDE_GRID_Y - layout.y);
966 drawVertical(layout.x + boardW, layout.y,
SIDE_GRID_Y - layout.y);
970 if (layout.gridIndex == 1) {
971 drawHorizontal(layout.x - thickness, layout.y - thickness, boardW + thickness);
972 drawHorizontal(layout.x - thickness, layout.y + boardH, boardW + thickness);
973 drawVertical(layout.x - thickness, layout.y, boardH);
977 if (layout.gridIndex == 2) {
979 const int exposedH = layout.y + boardH - exposedY;
980 drawHorizontal(layout.x - thickness, layout.y + boardH, boardW + thickness * 2);
981 drawVertical(layout.x - thickness, exposedY, exposedH);
982 drawVertical(layout.x + boardW, exposedY, exposedH);
986 drawHorizontal(layout.x, layout.y - thickness, boardW + thickness);
987 drawHorizontal(layout.x, layout.y + boardH, boardW + thickness);
988 drawVertical(layout.x + boardW, layout.y, boardH);
991 void MutatrisWindow::drawCell(
const MutatrisWindow::BoardLayout &layout,
int cellX,
int cellY,
int color) {
992 if (cellX < 0 || cellY < 0) {
995 const int spriteIndex = color < 0 ? 1 + (std::abs(color) % 6) : std::clamp(color, 0, 7);
996 mxvk::VK_Sprite *sprite = blocks[
static_cast<std::size_t
>(spriteIndex)];
997 if (sprite ==
nullptr) {
1004 if (layout.gridIndex == 0) {
1005 px = layout.x + cellX * BLOCK_WIDTH;
1006 py = layout.y + cellY * BLOCK_HEIGHT;
1007 }
else if (layout.gridIndex == 1) {
1008 px = layout.x + cellY * BLOCK_HEIGHT;
1009 py = layout.y + cellX * BLOCK_WIDTH;
1010 pw = BLOCK_DRAW_HEIGHT;
1011 ph = BLOCK_DRAW_WIDTH;
1012 }
else if (layout.gridIndex == 2) {
1013 px = layout.x + cellX * BLOCK_WIDTH;
1014 py = layout.y + (BOTTOM_GRID_HEIGHT - 1 - cellY) * BLOCK_HEIGHT;
1016 px = layout.x + (SIDE_GRID_HEIGHT - 1 - cellY) * BLOCK_HEIGHT;
1017 py = layout.y + (SIDE_GRID_WIDTH - 1 - cellX) * BLOCK_WIDTH;
1018 pw = BLOCK_DRAW_HEIGHT;
1019 ph = BLOCK_DRAW_WIDTH;
1021 const int scaledWidth = scaleX(pw);
1022 const int scaledHeight = scaleY(ph);
1025 const float rotation =
static_cast<float>(clearFrame) * (360.0f /
static_cast<float>(
CLEAR_ANIMATION_FRAMES));
1026 const float spriteScaleX =
static_cast<float>(scaledWidth) /
static_cast<float>(sprite->
getWidth());
1027 const float spriteScaleY =
static_cast<float>(scaledHeight) /
static_cast<float>(sprite->
getHeight());
1028 sprite->
drawSprite(scaleX(px), scaleY(py), spriteScaleX, spriteScaleY, rotation);
1030 sprite->
drawSpriteRect(scaleX(px), scaleY(py), scaledWidth, scaledHeight);
1034 float MutatrisWindow::layoutScale()
const {
1035 return std::max(0.2f, std::min(
static_cast<float>(width) /
static_cast<float>(
DESIGN_WIDTH),
static_cast<float>(height) /
static_cast<float>(
DESIGN_HEIGHT)));
1038 void MutatrisWindow::ensureUiFont() {
1039 const int scaledFontSize = std::max(1,
static_cast<int>(std::lround(24.0f * layoutScale())));
1040 if (scaledFontSize == uiFontSize) {
1043 uiFontSize = scaledFontSize;
1044 setFont(fontPath, uiFontSize);
1047 void MutatrisWindow::ensureIntroFonts() {
1048 const float scale = layoutScale();
1049 const int scaledBaseSize = std::max(1,
static_cast<int>(std::lround(54.0f * scale)));
1050 if (scaledBaseSize == introFontSize) {
1053 introFontSize = scaledBaseSize;
1054 resetScaledFont(titleFont, 44, scale);
1055 resetScaledFont(titleLargeFont, 220, scale);
1056 resetScaledFont(startMediumFont, 36, scale);
1057 resetScaledFont(startSmallFont, 18, scale);
1058 resetScaledFont(difficultyFont, 30, scale);
1061 void MutatrisWindow::resetScaledFont(mxvk::Font &font,
int designSize,
float scale) {
1062 font.
reset(fontPath, std::max(1,
static_cast<int>(std::lround(
static_cast<float>(designSize) * scale))));
1065 void MutatrisWindow::drawTextCentered(
const std::string &text,
int y, SDL_Color color) {
1069 textWidth =
static_cast<int>(text.size()) * 14;
1071 printScaledText(text, (
DESIGN_WIDTH - textWidth) / 2, y, color);
1074 void MutatrisWindow::drawTextCenteredInRect(
const std::string &text,
int x,
int y,
int w,
int h, SDL_Color color,
const mxvk::Font &font) {
1078 textWidth =
static_cast<int>(text.size()) * scaleX(10);
1079 textHeight = scaleY(18);
1082 const int rectX = scaleX(x);
1083 const int rectY = scaleY(y);
1084 const int rectW = scaleX(w);
1085 const int rectH = scaleY(h);
1086 const int textX = rectX + ((rectW - textWidth) / 2);
1087 const int textY = rectY + ((rectH - textHeight) / 2);
1088 printText(text, textX, textY, color, font);
1091 void MutatrisWindow::printScaledText(
const std::string &text,
int x,
int y, SDL_Color color) {
1092 printText(text, scaleX(x), scaleY(y), color);
1095 void MutatrisWindow::printScaledText(
const std::string &text,
int x,
int y, SDL_Color color,
const mxvk::Font &font) {
1096 printText(text, scaleX(x), scaleY(y), color, font);
1099 int MutatrisWindow::scaleX(
int value)
const {
1100 return static_cast<int>(
static_cast<float>(value) *
static_cast<float>(width) /
static_cast<float>(
DESIGN_WIDTH));
1103 int MutatrisWindow::scaleY(
int value)
const {
1104 return static_cast<int>(
static_cast<float>(value) *
static_cast<float>(height) /
static_cast<float>(
DESIGN_HEIGHT));
void event(SDL_Event &e) override
Handle one SDL event.
MutatrisWindow(const std::string &path, int width, int height, bool fullscreen, bool enableVsync, bool enableCrt)
void proc() override
Execute one processing/update step.
static int joysticks()
Return the number of connected joysticks/controllers.
void setShaderParams(float p1=0.0f, float p2=0.0f, float p3=0.0f, float p4=0.0f)
Set up to four custom shader float parameters.
void drawSprite(int x, int y)
Queue a draw at the given pixel position.
void setFragmentShaderPath(const std::string &path)
Replace the fragment shader path and rebuild the custom pipeline.
void drawSpriteRect(int x, int y, int w, int h)
Queue a draw into an explicit destination rectangle.
Main Vulkan window wrapper for MXVK.
VkExtent2D getSwapchainExtent() const noexcept
Get the current swapchain extent.
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.
void exit()
Request loop termination.
VK_Sprite * attachPostProcessingShader(const std::string &fragmentShaderPath, float p1=0.0f, float p2=0.0f, float p3=0.0f, float p4=0.0f)
Attach a full-screen post-processing fragment shader.
VK_Window()=default
Construct an empty window object.
void setFont(const std::string &fontPath, int fontSize=24)
Set the active text-render font.
void setPostProcessingEnabled(bool enabled)
void printText(const std::string &text, int x, int y, const SDL_Color &col)
Queue a text string for rendering during the current frame.
void setPostProcessingShaderTimeEnabled(bool enabled)
Keep shader param 1 updated with elapsed render time in seconds.
#define mutatris_ASSET_DIR
constexpr int START_PROMPT_RECT_W
constexpr int START_PROMPT_RECT_Y
constexpr int CONSOLE_DESIGN_FONT_SIZE
constexpr int START_PROMPT_RECT_H
constexpr int START_PROMPT_RECT_X
constexpr Uint32 STARTUP_FADE_MS
constexpr int BLOCK_HEIGHT
constexpr int SIDE_GRID_Y
constexpr int SIDE_GRID_WIDTH
constexpr int CENTER_GRID_X
constexpr Uint32 KEY_REPEAT_MS
constexpr int BLOCK_DRAW_WIDTH
constexpr int BOTTOM_GRID_Y
constexpr Uint32 CLEAR_ANIMATION_MS
constexpr int SIDE_GRID_HEIGHT
constexpr int CLEAR_ANIMATION_FRAMES
constexpr int BOTTOM_GRID_HEIGHT
constexpr int DESIGN_WIDTH
constexpr int DESIGN_HEIGHT
constexpr int TOP_GRID_HEIGHT
constexpr int RIGHT_GRID_X
constexpr Uint32 STARTUP_HOLD_MS
constexpr int BLOCK_DRAW_HEIGHT
constexpr int BLOCK_WIDTH
Utilities for loading and saving PNG images.