298 TetrisWindow(
const std::string &path,
int width,
int height,
bool fullscreen,
bool enable_vsync)
300 assetRoot((path.empty() || path ==
".") ? std::string(
tetris_ASSET_DIR) : path),
301 dataRoot(assetRoot +
"/data"),
302 shaderRoot(dataRoot),
304 std::random_device rd;
306 setFont(dataRoot +
"/font.ttf", 22);
308 tryOpenFirstGamepad();
309#if defined(MXVK_WITH_MIXER) || defined(WITH_MIXER)
310 music = std::make_unique<mxvk::VK_Mixer>();
311 musicTrack = music->loadMusic(dataRoot +
"/music.ogg");
312 ensureMusicPlaying();
314 background =
createSprite(dataRoot +
"/psychedelic_background.png");
316 dataRoot +
"/psychedelic_background.png",
317 shaderRoot +
"/sprite.vert.spv",
318 shaderRoot +
"/tetris_background_transition.frag.spv");
320 dataRoot +
"/start_screen.png",
321 shaderRoot +
"/sprite.vert.spv",
322 shaderRoot +
"/tetris_screen_fade.frag.spv");
324 dataRoot +
"/high_scores_screen.png",
325 shaderRoot +
"/sprite.vert.spv",
326 shaderRoot +
"/tetris_screen_fade.frag.spv");
328 dataRoot +
"/credits_screen.png",
329 shaderRoot +
"/sprite.vert.spv",
330 shaderRoot +
"/tetris_screen_fade.frag.spv");
332 dataRoot +
"/multiplayer_screen.png",
333 shaderRoot +
"/sprite.vert.spv",
334 shaderRoot +
"/tetris_screen_fade.frag.spv");
336 const uint32_t whitePixel = 0xFFFFFFFFu;
337 previewBorderSprite->updateTexture(&whitePixel, 1, 1);
338 for (std::size_t i = 0; i < blockPreviewSprites.size(); ++i) {
341 gameOverSprite =
createSprite(dataRoot +
"/gameover.png");
343 shaderRoot +
"/tetris_intro.vert.spv",
344 shaderRoot +
"/tetris_intro.frag.spv");
345 introStart = std::chrono::steady_clock::now();
352 if (
device != VK_NULL_HANDLE) {
355#if defined(MXVK_WITH_MIXER) || defined(WITH_MIXER)
361 closeMultiplayerSession();
373 if (e.type == SDL_EVENT_QUIT) {
378 if (e.type == SDL_EVENT_KEY_DOWN) {
383 handleNameEntryKey(e.key.key);
388 if (e.type == SDL_EVENT_GAMEPAD_ADDED) {
389 if (!openGamepad(e.gdevice.which)) {
390 tryOpenFirstGamepad();
395 if (e.type == SDL_EVENT_GAMEPAD_REMOVED) {
396 if (gamepad !=
nullptr && e.gdevice.which == gamepadId) {
398 tryOpenFirstGamepad();
403 if (e.type == SDL_EVENT_GAMEPAD_BUTTON_DOWN) {
405 handleNameEntryButton(e.gbutton.button);
408 handleGamepadButtonDown(e.gbutton.button);
413 ensureMusicPlaying();
414 tryOpenFirstGamepad();
416 updateScreenTransition();
417 updateMultiplayerNetwork();
427 drawScreenBackdrop(cmd, extent);
430 const float aspect = (extent.height > 0U) ?
static_cast<float>(extent.width) /
static_cast<float>(extent.height) : 1.0f;
431 glm::mat4 view = glm::lookAt(glm::vec3(0.0f, 0.2f, cameraDistance), glm::vec3(0.0f, 0.1f, 0.0f), glm::vec3(0.0f, 1.0f, 0.0f));
432 view = glm::rotate(view, glm::radians(gridPitch), glm::vec3(1.0f, 0.0f, 0.0f));
433 view = glm::rotate(view, glm::radians(gridYaw), glm::vec3(0.0f, 1.0f, 0.0f));
434 view = glm::rotate(view, glm::radians(gridRoll), glm::vec3(0.0f, 0.0f, 1.0f));
436 glm::mat4 proj = glm::perspective(glm::radians(46.0f), aspect, 0.1f, 100.0f);
438 const bool blinkVisible = !lineClearActive || isLineClearVisible();
441 if (blinkVisible || !isClearingRow(locked.y)) {
442 drawBlock(cmd, imageIndex, locked.block, locked.x, locked.y, locked.block.color, view, proj);
446 if (!lineClearActive) {
447 for (
size_t i = 0; i < activeModels.size(); ++i) {
448 const int x = active.x + active.blocks[i].x;
449 const int y = active.y + active.blocks[i].y;
450 drawBlock(cmd, imageIndex, activeModels[i], x, y, active.color, view, proj);
454 drawFrame(cmd, imageIndex, view, proj);
455 drawNextPiecePreview(cmd, imageIndex, extent);
456 drawOpponentGrid(cmd, extent);
459 drawCreditsModel(cmd, imageIndex, extent);
461 drawGameScreenTransitionOverlay(cmd, extent);
462 drawIntroOverlay(cmd, extent);
463 drawGameOverOverlay(cmd, extent);
467 std::string assetRoot;
468 std::string dataRoot;
469 std::string shaderRoot;
470 std::array<std::array<Cell, boardWidth>,
boardHeight> board{};
473 std::vector<LockedBlock> lockedBlocks{};
474 std::array<BlockModel, 4> activeModels{};
475 std::vector<BlockModel> frameModels{};
476 mxvk::VK_Sprite *background =
nullptr;
477 mxvk::VK_Sprite *backgroundTransitionSprite =
nullptr;
478 mxvk::VK_Sprite *menuBackgroundSprite =
nullptr;
479 mxvk::VK_Sprite *highScoresBackgroundSprite =
nullptr;
480 mxvk::VK_Sprite *creditsBackgroundSprite =
nullptr;
481 mxvk::VK_Sprite *multiplayerBackgroundSprite =
nullptr;
482 mxvk::VK_Sprite *previewBorderSprite =
nullptr;
483 std::array<mxvk::VK_Sprite *, 8> blockPreviewSprites{};
484 mxvk::VK_Sprite *introSprite =
nullptr;
485 mxvk::VK_Sprite *gameOverSprite =
nullptr;
486 std::unique_ptr<mxvk::VKAbstractModel> creditsTuxModel{};
488#if defined(MXVK_WITH_MIXER) || defined(WITH_MIXER)
489 std::unique_ptr<mxvk::VK_Mixer> music{};
492 SDL_Gamepad *gamepad =
nullptr;
493 SDL_JoystickID gamepadId = 0;
494 std::chrono::steady_clock::time_point introStart{std::chrono::steady_clock::now()};
495 std::chrono::steady_clock::time_point lastFall{std::chrono::steady_clock::now()};
496 std::chrono::steady_clock::time_point lastInputUpdate{std::chrono::steady_clock::now()};
497 std::chrono::steady_clock::time_point lineClearStart{std::chrono::steady_clock::now()};
498 std::chrono::steady_clock::time_point backgroundTransitionStart{std::chrono::steady_clock::now()};
499 std::chrono::steady_clock::time_point screenTransitionStart{std::chrono::steady_clock::now()};
500 std::chrono::steady_clock::time_point gameOverTransitionStart{std::chrono::steady_clock::now()};
501 bool introActive =
true;
502 float gridYaw = 0.0f;
503 float gridPitch = 0.0f;
504 float gridRoll = 0.0f;
505 float cameraDistance = 2.45f;
507 int linesCleared = 0;
510 float fallSeconds = 0.65f;
511 float moveRepeatTimer = 0.0f;
512 float softDropRepeatTimer = 0.0f;
513 float gamepadMoveRepeatTimer = 0.0f;
514 float gamepadSoftDropRepeatTimer = 0.0f;
515 bool gameOver =
false;
516 bool enteringName =
false;
517 bool highScoresAfterSave =
false;
518 std::size_t nameCharIndex = 0;
519 std::string playerName{};
520 bool hardDropHeld =
false;
521 bool rotateHeld =
false;
522 bool resetHeld =
false;
523 bool enterHeld =
false;
524 bool escapeHeld =
false;
525 bool backspaceHeld =
false;
526 bool menuUpHeld =
false;
527 bool menuDownHeld =
false;
528 bool menuEnterHeld =
false;
529 bool introSkipHeld =
false;
530 bool introFadeStarted =
false;
531 bool screenTransitionActive =
false;
532 bool gameOverTransitionActive =
false;
533 bool lineClearActive =
false;
534 bool backgroundTransitionActive =
false;
535 std::array<bool, boardHeight> clearingRows{};
536 PieceQueueEntry nextPiece{};
539 mxnetwork::MXNetworkInit networkInit{};
540 mxnetwork::Socket listenSocket{};
541 mxnetwork::Socket peerSocket{};
542 MultiplayerSnapshot opponentSnapshot{};
544 std::string multiplayerHost{
"127.0.0.1"};
545 std::string multiplayerStatus{
"Enter host IP, then press J. Press H to host."};
546 std::string multiplayerResult{};
547 std::string networkReceiveBuffer{};
548 std::chrono::steady_clock::time_point lastNetworkSend{std::chrono::steady_clock::now()};
549 std::uint32_t networkSequence = 0;
550 int multiplayerCursor = 0;
551 bool multiplayerActive =
false;
552 bool multiplayerHostSide =
false;
553 bool hostHeld =
false;
554 bool joinHeld =
false;
555 static constexpr float introHoldSeconds = 5.0f;
556 static constexpr float introFadeSeconds = 1.0f;
557 static constexpr float screenTransitionSeconds = 0.36f;
558 static constexpr float gameOverTransitionSeconds = 0.5f;
559 static constexpr float backgroundTransitionSeconds = 1.25f;
560 static constexpr Sint16 gamepadDeadzone = 10000;
561 static constexpr float gamepadMoveInitialDelaySeconds = 0.22f;
562 static constexpr float gamepadMoveRepeatSeconds = 0.12f;
563 static constexpr float gamepadSoftDropInitialDelaySeconds = 0.18f;
564 static constexpr float gamepadSoftDropRepeatSeconds = 0.08f;
565 static constexpr float gamepadStickRotateSpeed = 120.0f;
566 static constexpr float gamepadStickPitchSpeed = 100.0f;
567 static constexpr float gamepadStickScale = 1.0f / 32768.0f;
568 int gamepadMoveDirection = 0;
569 float gamepadMoveHeldSeconds = 0.0f;
570 bool gamepadSoftDropHeld =
false;
573 frameModels.reserve(boardWidth + (boardHeight * 2) + 2);
575 frameModels.push_back(loadBlockModel(7));
579 [[nodiscard]] BlockModel loadBlockModel(
int color) {
582 block.model = std::make_unique<mxvk::VKAbstractModel>();
583 block.model->load(
this, dataRoot +
"/cube.mxmod.z", dataRoot +
"/" + textureManifests[color], dataRoot, 1.0f);
584 block.model->setShaders(
this,
585 shaderRoot +
"/tetris_piece.vert.spv",
586 shaderRoot +
"/tetris_piece.frag.spv");
590 void cleanupModels() {
591 forEachModel([
this](mxvk::VKAbstractModel &model) {
594 lockedBlocks.clear();
597 template <
typename Fn>
598 void forEachModel(Fn fn) {
599 for (LockedBlock &locked : lockedBlocks) {
600 if (locked.block.model) {
601 fn(*locked.block.model);
604 for (BlockModel &block : activeModels) {
609 for (BlockModel &block : frameModels) {
614 if (creditsTuxModel) {
615 fn(*creditsTuxModel);
619 void initCreditsModel() {
620 creditsTuxModel = std::make_unique<mxvk::VKAbstractModel>();
621 creditsTuxModel->load(
this,
622 dataRoot +
"/tux/tux.obj",
623 dataRoot +
"/tux/tux.mtl",
626 creditsTuxModel->setShaders(
this,
627 shaderRoot +
"/tetris_model.vert.spv",
628 shaderRoot +
"/tetris_model.frag.spv");
634 for (
auto &row : board) {
635 for (Cell &cell : row) {
640 gameOverTransitionActive =
false;
641 multiplayerResult.clear();
642 lineClearActive =
false;
643 backgroundTransitionActive =
false;
644 clearingRows.fill(
false);
648 lastFall = std::chrono::steady_clock::now();
649 nextPiece = randomPiece();
653 void resetOpponentSnapshot() {
654 opponentSnapshot = MultiplayerSnapshot{};
655 networkReceiveBuffer.clear();
658 void closeMultiplayerSession(std::string status =
"Enter host IP, then press J. Press H to host.") {
659 if (peerSocket.valid()) {
662 if (listenSocket.valid()) {
663 listenSocket.close();
665 multiplayerActive =
false;
666 multiplayerHostSide =
false;
668 multiplayerStatus = std::move(status);
669 resetOpponentSnapshot();
672 [[nodiscard]] std::string multiplayerOutcomeText()
const {
673 if (gameOver && !opponentSnapshot.gameOver) {
674 return "You lost. Opponent won.";
676 if (!gameOver && opponentSnapshot.gameOver) {
677 return "You won. Opponent lost.";
679 if (score > opponentSnapshot.score) {
680 return "You won by score.";
682 if (score < opponentSnapshot.score) {
683 return "You lost by score.";
688 void enterMultiplayerGameOver(std::string reason) {
689 const std::string outcome = multiplayerOutcomeText();
690 closeMultiplayerSession(std::move(reason));
691 multiplayerResult = outcome;
694 gameOverTransitionActive =
true;
695 gameOverTransitionStart = std::chrono::steady_clock::now();
696 enteringName =
false;
697 highScoresAfterSave =
false;
698 resetMenuLatchState();
701 void finishLocalMultiplayerGameOver() {
702 multiplayerResult =
"You lost. Opponent won.";
703 sendMultiplayerSnapshot(
true);
704 closeMultiplayerSession(
"Game over.");
705 enteringName =
false;
706 highScoresAfterSave =
false;
709 void beginHosting() {
710 closeMultiplayerSession();
712 if (!listenSocket.listen(multiplayerPort, 1)) {
714 multiplayerStatus = std::format(
"Could not host on port {}", multiplayerPort);
717 listenSocket.setblocking(
false);
719 multiplayerHostSide =
true;
720 multiplayerStatus = std::format(
"Hosting on port {}. Waiting for peer...", multiplayerPort);
724 closeMultiplayerSession();
725 if (multiplayerHost.empty()) {
727 multiplayerStatus =
"Enter an IP address before joining.";
732 multiplayerStatus = std::format(
"Connecting to {}:{}...", multiplayerHost, multiplayerPort);
733 if (!peerSocket.connect(multiplayerHost, multiplayerPort)) {
736 multiplayerStatus = std::format(
"Could not connect to {}:{}", multiplayerHost, multiplayerPort);
739 peerSocket.setblocking(
false);
740 multiplayerHostSide =
false;
741 finishMultiplayerConnection(
"Connected as guest.");
744 void finishMultiplayerConnection(
const std::string &status) {
745 if (listenSocket.valid()) {
746 listenSocket.close();
749 multiplayerStatus = status;
750 multiplayerActive =
true;
751 resetOpponentSnapshot();
755 screenTransitionActive =
false;
757 lastInputUpdate = std::chrono::steady_clock::now();
758 lastNetworkSend = std::chrono::steady_clock::now();
759 resetMenuLatchState();
760 sendMultiplayerSnapshot(
true);
763 void updateMultiplayerNetwork() {
766 std::optional<mxnetwork::Socket> accepted = listenSocket.accept();
768 peerSocket = std::move(*accepted);
769 peerSocket.setblocking(
false);
770 finishMultiplayerConnection(
"Peer connected. Multiplayer started.");
772 }
catch (
const mxnetwork::Exception &ex) {
774 multiplayerStatus = std::format(
"Accept failed: {}", ex.
text());
782 receiveMultiplayerData();
784 const auto now = std::chrono::steady_clock::now();
785 const float elapsed = std::chrono::duration<float>(now - lastNetworkSend).count();
786 if (elapsed >= 0.05f) {
787 sendMultiplayerSnapshot(
false);
788 lastNetworkSend = now;
792 std::string makeMultiplayerSnapshotLine() {
793 std::ostringstream out;
794 out <<
"S " << networkSequence++ <<
' ' << score <<
' ' << linesCleared <<
' ' << level <<
' ' << (gameOver ? 1 : 0) <<
' ';
797 const int color = board[y][x].color;
798 out << ((color >= 0 && color <= 7) ? static_cast<char>(
'0' + color) :
'.');
801 out <<
' ' << active.color <<
' ' << active.x <<
' ' << active.y;
802 for (
const Block &block : active.blocks) {
803 out <<
' ' << block.x <<
' ' << block.y;
809 void sendMultiplayerSnapshot(
bool force) {
816 const std::string line = makeMultiplayerSnapshotLine();
817 const ssize_t written = peerSocket.write(line.data(), line.size(), 0);
819 enterMultiplayerGameOver(
"Peer disconnected.");
823 void receiveMultiplayerData() {
824 std::array<char, 2048> buffer{};
825 while (peerSocket.valid()) {
826 const ssize_t received = peerSocket.read(buffer.data(), buffer.size(), 0);
828 networkReceiveBuffer.append(buffer.data(),
static_cast<std::size_t
>(received));
829 consumeMultiplayerLines();
833 enterMultiplayerGameOver(
"Peer disconnected.");
839 void consumeMultiplayerLines() {
840 std::size_t newline = networkReceiveBuffer.find(
'\n');
841 while (newline != std::string::npos) {
842 const std::string line = networkReceiveBuffer.substr(0, newline);
843 networkReceiveBuffer.erase(0, newline + 1);
844 parseMultiplayerSnapshot(line);
845 newline = networkReceiveBuffer.find(
'\n');
847 constexpr std::size_t maxBufferedBytes = 8192;
848 if (networkReceiveBuffer.size() > maxBufferedBytes) {
849 networkReceiveBuffer.clear();
853 void parseMultiplayerSnapshot(
const std::string &line) {
854 std::istringstream in(line);
856 [[maybe_unused]] std::uint32_t sequence = 0;
858 int remoteGameOver = 0;
859 MultiplayerSnapshot snapshot{};
861 in >> type >> sequence >> snapshot.score >> snapshot.lines >> snapshot.level >> remoteGameOver >> cells;
862 if (type !=
'S' || cells.size() !=
static_cast<std::size_t
>(boardWidth * boardHeight)) {
866 snapshot.gameOver = remoteGameOver != 0;
869 const char ch = cells[
static_cast<std::size_t
>(y *
boardWidth + x)];
870 snapshot.cells[y][x] = (ch >=
'0' && ch <=
'7') ? ch -
'0' : -1;
874 in >> snapshot.activeColor >> snapshot.activeX >> snapshot.activeY;
875 for (Block &block : snapshot.activeBlocks) {
876 in >> block.x >> block.y;
881 snapshot.activeColor = std::clamp(snapshot.activeColor, 0, 7);
882 snapshot.hasState =
true;
883 opponentSnapshot = snapshot;
886 void clearLockedBlocks() {
887 for (LockedBlock &locked : lockedBlocks) {
888 if (locked.block.model) {
889 locked.block.model->cleanup(
this);
892 lockedBlocks.clear();
896 active.blocks = nextPiece.blocks;
899 active.color = nextPiece.color;
900 reloadActiveModels(active.color);
901 gameOver = collides(active.x, active.y, active.blocks);
903 enterGameOverState();
905 nextPiece = randomPiece();
908 void reloadActiveModels(
int color) {
909 for (BlockModel &block : activeModels) {
911 block.model->cleanup(
this);
913 block = loadBlockModel(color);
917 [[nodiscard]] PieceQueueEntry randomPiece() {
918 std::uniform_int_distribution<int> dist(0,
static_cast<int>(
pieceDefinitions.size()) - 1);
920 return PieceQueueEntry{definition.blocks, definition.color};
923 void updateDifficulty() {
924 const int previousLevel = level;
925 level = (linesCleared / 8) + 1;
926 static constexpr std::array<float, 16> arcadeFallSeconds{
945 const std::size_t index = std::min<std::size_t>(arcadeFallSeconds.size() - 1,
static_cast<std::size_t
>(std::max(level - 1, 0)));
946 fallSeconds = arcadeFallSeconds[index];
947 if (level > previousLevel) {
948 triggerBackgroundTransition();
952#if defined(MXVK_WITH_MIXER) || defined(WITH_MIXER)
953 void ensureMusicPlaying() {
957 if (musicTrack < 0) {
960 if (!music->isMusicPlaying(musicTrack)) {
961 if (music->playMusic(musicTrack, -1) != 0) {
962 throw mxvk::Exception(
"Could not start Tetris background music");
967 void ensureMusicPlaying() {}
971 const auto now = std::chrono::steady_clock::now();
972 float deltaSeconds = std::chrono::duration<float>(now - lastInputUpdate).count();
973 lastInputUpdate = now;
974 deltaSeconds = std::clamp(deltaSeconds, 0.0f, 0.05f);
976 const bool *keys = SDL_GetKeyboardState(
nullptr);
977 if (keys ==
nullptr) {
982 const bool enterDown = keys[SDL_SCANCODE_RETURN];
983 const bool spaceDown = keys[SDL_SCANCODE_SPACE];
984 const bool escapeDown = keys[SDL_SCANCODE_ESCAPE];
985 const bool skipDown = enterDown || spaceDown;
986 if (skipDown && !introSkipHeld) {
987 const auto now = std::chrono::steady_clock::now();
988 introStart = now - std::chrono::duration_cast<std::chrono::steady_clock::duration>(std::chrono::duration<float>(introHoldSeconds));
989 introFadeStarted =
false;
992 if (escapeDown && !escapeHeld) {
995 introSkipHeld = skipDown;
996 escapeHeld = escapeDown;
1000 if (screenTransitionActive) {
1008 handleGameOverKeys(keys);
1013 handleMultiplayerKeys(keys);
1018 handleMenuKeys(keys);
1022 handleHeldViewRotation(keys, deltaSeconds);
1023 handleHeldPieceMovement(keys, deltaSeconds);
1024 handleOneShotKeys(keys);
1025 handleGamepadInput(deltaSeconds);
1028 void handleMenuKeys(
const bool *keys) {
1029 if (screenTransitionActive) {
1030 menuUpHeld = keys[SDL_SCANCODE_UP];
1031 menuDownHeld = keys[SDL_SCANCODE_DOWN];
1032 menuEnterHeld = keys[SDL_SCANCODE_RETURN];
1033 escapeHeld = keys[SDL_SCANCODE_ESCAPE];
1037 const bool upDown = keys[SDL_SCANCODE_UP];
1038 const bool downDown = keys[SDL_SCANCODE_DOWN];
1039 const bool enterDown = keys[SDL_SCANCODE_RETURN];
1040 const bool escapeDown = keys[SDL_SCANCODE_ESCAPE];
1043 if (upDown && !menuUpHeld) {
1044 cursorPos = (cursorPos + 3) % 4;
1046 if (downDown && !menuDownHeld) {
1047 cursorPos = (cursorPos + 1) % 4;
1050 if (enterDown && !menuEnterHeld) {
1052 if (cursorPos == 0) {
1054 }
else if (cursorPos == 1) {
1055 goToNetworkMultiplayer();
1056 }
else if (cursorPos == 2) {
1058 }
else if (cursorPos == 3) {
1062 highScoresAfterSave =
false;
1063 restartIntroSequence();
1068 if (escapeDown && !escapeHeld) {
1072 highScoresAfterSave =
false;
1077 menuUpHeld = upDown;
1078 menuDownHeld = downDown;
1079 menuEnterHeld = enterDown;
1080 escapeHeld = escapeDown;
1083 void handleMultiplayerKeys(
const bool *keys) {
1084 const bool escapeDown = keys[SDL_SCANCODE_ESCAPE];
1085 const bool enterDown = keys[SDL_SCANCODE_RETURN];
1086 const bool hostDown = keys[SDL_SCANCODE_H];
1087 const bool joinDown = keys[SDL_SCANCODE_J];
1088 const bool backspaceDown = keys[SDL_SCANCODE_BACKSPACE];
1090 if (escapeDown && !escapeHeld) {
1091 closeMultiplayerSession();
1094 if (hostDown && !hostHeld) {
1097 if (joinDown && !joinHeld) {
1100 if (enterDown && !menuEnterHeld) {
1103 if (backspaceDown && !backspaceHeld && !multiplayerHost.empty()) {
1104 multiplayerHost.pop_back();
1107 appendMultiplayerAddressCharacters(keys);
1109 escapeHeld = escapeDown;
1110 menuEnterHeld = enterDown;
1111 hostHeld = hostDown;
1112 joinHeld = joinDown;
1113 backspaceHeld = backspaceDown;
1116 void appendMultiplayerAddressCharacters(
const bool *keys) {
1117 static constexpr std::array<SDL_Scancode, 10> digitKeys{{
1129 static constexpr std::array<SDL_Scancode, 10> keypadDigitKeys{{
1142 auto appendChar = [
this](
char ch) {
1143 constexpr std::size_t maxAddressLength = 64;
1144 if (multiplayerHost.size() < maxAddressLength) {
1145 multiplayerHost.push_back(ch);
1149 for (
int i = 0; i < 10; ++i) {
1150 if ((keys[digitKeys[
static_cast<std::size_t
>(i)]] || keys[keypadDigitKeys[
static_cast<std::size_t
>(i)]]) && multiplayerCursor != (i + 1)) {
1151 appendChar(
static_cast<char>(
'0' + i));
1152 multiplayerCursor = i + 1;
1156 if ((keys[SDL_SCANCODE_PERIOD] || keys[SDL_SCANCODE_KP_PERIOD]) && multiplayerCursor != 11) {
1158 multiplayerCursor = 11;
1161 if (keys[SDL_SCANCODE_MINUS] && multiplayerCursor != 12) {
1163 multiplayerCursor = 12;
1166 if (!keys[SDL_SCANCODE_PERIOD] && !keys[SDL_SCANCODE_KP_PERIOD] && !keys[SDL_SCANCODE_MINUS]) {
1167 bool anyDigitDown =
false;
1168 for (
int i = 0; i < 10; ++i) {
1169 anyDigitDown = anyDigitDown || keys[digitKeys[
static_cast<std::size_t
>(i)]] || keys[keypadDigitKeys[
static_cast<std::size_t
>(i)]];
1171 if (!anyDigitDown) {
1172 multiplayerCursor = 0;
1177 void handleGamepadInput(
float deltaSeconds) {
1179 gamepadMoveDirection = 0;
1180 gamepadMoveHeldSeconds = 0.0f;
1181 gamepadSoftDropHeld =
false;
1182 gamepadMoveRepeatTimer = 0.0f;
1183 gamepadSoftDropRepeatTimer = 0.0f;
1187 const Sint16 leftX = SDL_GetGamepadAxis(gamepad, SDL_GAMEPAD_AXIS_LEFTX);
1188 const Sint16 leftY = SDL_GetGamepadAxis(gamepad, SDL_GAMEPAD_AXIS_LEFTY);
1189 const Sint16 rightX = SDL_GetGamepadAxis(gamepad, SDL_GAMEPAD_AXIS_RIGHTX);
1190 const Sint16 rightY = SDL_GetGamepadAxis(gamepad, SDL_GAMEPAD_AXIS_RIGHTY);
1192 const int moveDirection = (leftX < -gamepadDeadzone) ? -1 : (leftX > gamepadDeadzone) ? 1
1194 if (moveDirection == 0) {
1195 gamepadMoveDirection = 0;
1196 gamepadMoveHeldSeconds = 0.0f;
1197 gamepadMoveRepeatTimer = 0.0f;
1199 if (moveDirection != gamepadMoveDirection) {
1200 gamepadMoveDirection = moveDirection;
1201 gamepadMoveHeldSeconds = 0.0f;
1202 gamepadMoveRepeatTimer = 0.0f;
1203 movePiece(gamepadMoveDirection, 0);
1205 gamepadMoveHeldSeconds += deltaSeconds;
1206 const float threshold = (gamepadMoveHeldSeconds < gamepadMoveInitialDelaySeconds)
1207 ? gamepadMoveInitialDelaySeconds
1208 : gamepadMoveRepeatSeconds;
1209 gamepadMoveRepeatTimer += deltaSeconds;
1210 if (gamepadMoveRepeatTimer >= threshold) {
1211 movePiece(gamepadMoveDirection, 0);
1212 gamepadMoveRepeatTimer = 0.0f;
1217 const bool softDropDown = leftY > gamepadDeadzone;
1218 if (!softDropDown) {
1219 gamepadSoftDropHeld =
false;
1220 gamepadSoftDropRepeatTimer = 0.0f;
1222 const float threshold = gamepadSoftDropHeld ? gamepadSoftDropRepeatSeconds : gamepadSoftDropInitialDelaySeconds;
1223 gamepadSoftDropRepeatTimer += deltaSeconds;
1224 if (gamepadSoftDropRepeatTimer >= threshold) {
1226 gamepadSoftDropRepeatTimer = 0.0f;
1227 lastFall = std::chrono::steady_clock::now();
1228 gamepadSoftDropHeld =
true;
1232 if (std::abs(rightX) > gamepadDeadzone) {
1233 gridYaw +=
static_cast<float>(rightX) * gamepadStickScale * gamepadStickRotateSpeed * deltaSeconds;
1235 if (std::abs(rightY) > gamepadDeadzone) {
1236 gridPitch = std::clamp(gridPitch -
static_cast<float>(rightY) * gamepadStickScale * gamepadStickPitchSpeed * deltaSeconds,
1241 constexpr float gamepadZoomSpeed = 3.2f;
1242 if (SDL_GetGamepadButton(gamepad, SDL_GAMEPAD_BUTTON_LEFT_SHOULDER)) {
1243 cameraDistance = std::min(9.0f, cameraDistance + gamepadZoomSpeed * deltaSeconds);
1245 if (SDL_GetGamepadButton(gamepad, SDL_GAMEPAD_BUTTON_RIGHT_SHOULDER)) {
1246 cameraDistance = std::max(1.65f, cameraDistance - gamepadZoomSpeed * deltaSeconds);
1250 void handleGamepadButtonDown(Uint8 button) {
1251 if (gamepad ==
nullptr) {
1254 if (screenTransitionActive) {
1259 if (button == SDL_GAMEPAD_BUTTON_SOUTH || button == SDL_GAMEPAD_BUTTON_START) {
1260 introActive =
false;
1268 if (button == SDL_GAMEPAD_BUTTON_DPAD_UP) {
1269 cursorPos = (cursorPos + 3) % 4;
1270 }
else if (button == SDL_GAMEPAD_BUTTON_DPAD_DOWN) {
1271 cursorPos = (cursorPos + 1) % 4;
1272 }
else if (button == SDL_GAMEPAD_BUTTON_SOUTH || button == SDL_GAMEPAD_BUTTON_START) {
1273 if (cursorPos == 0) {
1275 }
else if (cursorPos == 1) {
1276 goToNetworkMultiplayer();
1277 }
else if (cursorPos == 2) {
1279 }
else if (cursorPos == 3) {
1282 }
else if (button == SDL_GAMEPAD_BUTTON_BACK || button == SDL_GAMEPAD_BUTTON_EAST) {
1288 if (button == SDL_GAMEPAD_BUTTON_START || button == SDL_GAMEPAD_BUTTON_SOUTH) {
1289 restartIntroSequence();
1290 }
else if (button == SDL_GAMEPAD_BUTTON_BACK || button == SDL_GAMEPAD_BUTTON_EAST) {
1295 if (lineClearActive) {
1298 if (button == SDL_GAMEPAD_BUTTON_SOUTH) {
1300 }
else if (button == SDL_GAMEPAD_BUTTON_EAST) {
1302 }
else if (button == SDL_GAMEPAD_BUTTON_BACK) {
1307 if (button == SDL_GAMEPAD_BUTTON_SOUTH || button == SDL_GAMEPAD_BUTTON_START) {
1311 restartIntroSequence();
1313 }
else if (button == SDL_GAMEPAD_BUTTON_BACK || button == SDL_GAMEPAD_BUTTON_EAST) {
1314 highScoresAfterSave =
false;
1321 if (button == SDL_GAMEPAD_BUTTON_SOUTH || button == SDL_GAMEPAD_BUTTON_START || button == SDL_GAMEPAD_BUTTON_BACK || button == SDL_GAMEPAD_BUTTON_EAST) {
1322 highScoresAfterSave =
false;
1331 bool openGamepad(SDL_JoystickID
id) {
1332 if (gamepad !=
nullptr && gamepadId ==
id) {
1336 gamepad = SDL_OpenGamepad(
id);
1337 if (gamepad ==
nullptr) {
1344 void closeGamepad() {
1345 if (gamepad !=
nullptr) {
1346 SDL_CloseGamepad(gamepad);
1352 void tryOpenFirstGamepad() {
1353 if (gamepad !=
nullptr) {
1357 SDL_JoystickID *ids = SDL_GetGamepads(&count);
1358 if (ids ==
nullptr || count <= 0) {
1359 if (ids !=
nullptr) {
1364 openGamepad(ids[0]);
1368 void updateIntroState() {
1373 const auto now = std::chrono::steady_clock::now();
1374 const float elapsed = std::chrono::duration<float>(now - introStart).count();
1375 if (!introFadeStarted && elapsed >= introHoldSeconds) {
1376 introFadeStarted =
true;
1379 if (elapsed >= (introHoldSeconds + introFadeSeconds)) {
1380 introActive =
false;
1381 introFadeStarted =
false;
1387 const AppScreen previousScreen = screen;
1388 closeMultiplayerSession();
1390 introActive =
false;
1393 transitionFromScreen = previousScreen;
1394 screenTransitionStart = std::chrono::steady_clock::now();
1395 lastInputUpdate = std::chrono::steady_clock::now();
1396 resetMenuLatchState();
1399 void restartIntroSequence() {
1401 highScoresAfterSave =
false;
1403 introFadeStarted =
false;
1404 introStart = std::chrono::steady_clock::now();
1406 screenTransitionActive =
false;
1408 lastInputUpdate = std::chrono::steady_clock::now();
1409 resetMenuLatchState();
1410 introSkipHeld =
true;
1413 void enterGameOverState() {
1415 gameOverTransitionActive =
true;
1416 gameOverTransitionStart = std::chrono::steady_clock::now();
1419 if (multiplayerActive) {
1420 finishLocalMultiplayerGameOver();
1421 resetMenuLatchState();
1424 enteringName = highScores.qualifies(score);
1425 highScoresAfterSave =
false;
1426 resetMenuLatchState();
1429 void stopNameEntry() {
1430 enteringName =
false;
1435 void commitScoreEntry() {
1436 highScores.addScore(playerName, score);
1438 highScoresAfterSave =
true;
1439 menuEnterHeld =
true;
1443 [[nodiscard]]
char currentNameCharacter()
const {
1447 void cycleNameCharacter(
int delta) {
1448 if (!enteringName) {
1452 const int next = (
static_cast<int>(nameCharIndex) + delta + count) % count;
1453 nameCharIndex =
static_cast<std::size_t
>(next);
1456 void appendCurrentNameCharacter() {
1457 if (!enteringName || playerName.size() >= nameMaxLength) {
1460 playerName += currentNameCharacter();
1463 void deleteNameCharacter() {
1464 if (!enteringName || playerName.empty()) {
1467 playerName.pop_back();
1470 void handleNameEntryKey(SDL_Keycode key) {
1471 if (key == SDLK_UP) {
1472 cycleNameCharacter(-1);
1473 }
else if (key == SDLK_DOWN) {
1474 cycleNameCharacter(1);
1475 }
else if (key == SDLK_SPACE) {
1476 appendCurrentNameCharacter();
1477 }
else if (key == SDLK_BACKSPACE) {
1478 deleteNameCharacter();
1479 }
else if (key == SDLK_RETURN) {
1481 }
else if (key == SDLK_ESCAPE) {
1482 highScoresAfterSave =
false;
1487 void handleNameEntryButton(Uint8 button) {
1488 if (button == SDL_GAMEPAD_BUTTON_DPAD_UP) {
1489 cycleNameCharacter(-1);
1490 }
else if (button == SDL_GAMEPAD_BUTTON_DPAD_DOWN) {
1491 cycleNameCharacter(1);
1492 }
else if (button == SDL_GAMEPAD_BUTTON_SOUTH) {
1493 appendCurrentNameCharacter();
1494 }
else if (button == SDL_GAMEPAD_BUTTON_WEST) {
1496 }
else if (button == SDL_GAMEPAD_BUTTON_EAST || button == SDL_GAMEPAD_BUTTON_BACK) {
1497 deleteNameCharacter();
1506 closeMultiplayerSession();
1508 gameOverTransitionActive =
false;
1509 highScoresAfterSave =
false;
1515 void goToHighScores() {
1521 void goToCredits() {
1525 void goToNetworkMultiplayer() {
1529 void requestScreen(AppScreen nextScreen) {
1530 if (screen == nextScreen && !screenTransitionActive) {
1534 screenTransitionActive =
false;
1535 screen = nextScreen;
1539 screenTransitionActive =
false;
1540 screen = nextScreen;
1543 transitionFromScreen = screen;
1544 screen = nextScreen;
1545 screenTransitionActive =
true;
1546 screenTransitionStart = std::chrono::steady_clock::now();
1547 resetMenuLatchState();
1550 void updateScreenTransition() {
1551 if (!screenTransitionActive) {
1554 const auto now = std::chrono::steady_clock::now();
1555 const float elapsed = std::chrono::duration<float>(now - screenTransitionStart).count();
1556 if (elapsed >= screenTransitionSeconds) {
1557 screenTransitionActive =
false;
1558 transitionFromScreen = screen;
1559 resetMenuLatchState();
1563 float screenFadeAlpha()
const {
1564 if (!screenTransitionActive) {
1567 const auto now = std::chrono::steady_clock::now();
1568 const float elapsed = std::chrono::duration<float>(now - screenTransitionStart).count();
1569 return std::clamp(elapsed / screenTransitionSeconds, 0.0f, 1.0f);
1572 float gameOverFadeAlpha()
const {
1573 if (!gameOverTransitionActive) {
1576 const auto now = std::chrono::steady_clock::now();
1577 const float elapsed = std::chrono::duration<float>(now - gameOverTransitionStart).count();
1578 return std::clamp(elapsed / gameOverTransitionSeconds, 0.0f, 1.0f);
1581 void resetMenuLatchState() {
1583 menuDownHeld =
false;
1584 menuEnterHeld =
false;
1585 introSkipHeld =
false;
1586 backspaceHeld =
false;
1589 multiplayerCursor = 0;
1592 void updateMenuInputLatchState() {
1593 const bool *keys = SDL_GetKeyboardState(
nullptr);
1594 if (keys ==
nullptr) {
1597 menuUpHeld = keys[SDL_SCANCODE_UP];
1598 menuDownHeld = keys[SDL_SCANCODE_DOWN];
1599 menuEnterHeld = keys[SDL_SCANCODE_RETURN];
1600 escapeHeld = keys[SDL_SCANCODE_ESCAPE];
1603 void handleHeldViewRotation(
const bool *keys,
float deltaSeconds) {
1604 constexpr float yawSpeed = 115.0f;
1605 constexpr float pitchSpeed = 90.0f;
1606 constexpr float rollSpeed = 100.0f;
1607 constexpr float zoomSpeed = 3.2f;
1609 if (keys[SDL_SCANCODE_A]) {
1610 gridYaw -= yawSpeed * deltaSeconds;
1612 if (keys[SDL_SCANCODE_D]) {
1613 gridYaw += yawSpeed * deltaSeconds;
1615 if (keys[SDL_SCANCODE_W]) {
1616 gridPitch = std::clamp(gridPitch + pitchSpeed * deltaSeconds, -70.0f, 70.0f);
1618 if (keys[SDL_SCANCODE_S]) {
1619 gridPitch = std::clamp(gridPitch - pitchSpeed * deltaSeconds, -70.0f, 70.0f);
1621 if (keys[SDL_SCANCODE_Q]) {
1622 gridRoll -= rollSpeed * deltaSeconds;
1624 if (keys[SDL_SCANCODE_E]) {
1625 gridRoll += rollSpeed * deltaSeconds;
1627 if (keys[SDL_SCANCODE_PAGEUP]) {
1628 cameraDistance = std::max(1.65f, cameraDistance - zoomSpeed * deltaSeconds);
1630 if (keys[SDL_SCANCODE_PAGEDOWN]) {
1631 cameraDistance = std::min(9.0f, cameraDistance + zoomSpeed * deltaSeconds);
1635 void handleHeldPieceMovement(
const bool *keys,
float deltaSeconds) {
1636 if (gameOver || lineClearActive) {
1637 moveRepeatTimer = 0.0f;
1638 softDropRepeatTimer = 0.0f;
1642 constexpr float horizontalRepeatSeconds = 0.14f;
1643 constexpr float softDropRepeatSeconds = 0.075f;
1645 moveRepeatTimer += deltaSeconds;
1646 softDropRepeatTimer += deltaSeconds;
1648 int horizontalDirection = 0;
1649 if (keys[SDL_SCANCODE_LEFT] && !keys[SDL_SCANCODE_RIGHT]) {
1650 horizontalDirection = -1;
1651 }
else if (keys[SDL_SCANCODE_RIGHT] && !keys[SDL_SCANCODE_LEFT]) {
1652 horizontalDirection = 1;
1655 if (horizontalDirection != 0) {
1656 if (moveRepeatTimer >= horizontalRepeatSeconds) {
1657 movePiece(horizontalDirection, 0);
1658 moveRepeatTimer = 0.0f;
1661 moveRepeatTimer = horizontalRepeatSeconds;
1664 if (keys[SDL_SCANCODE_DOWN]) {
1665 if (softDropRepeatTimer >= softDropRepeatSeconds) {
1667 softDropRepeatTimer = 0.0f;
1668 lastFall = std::chrono::steady_clock::now();
1671 softDropRepeatTimer = softDropRepeatSeconds;
1675 void handleOneShotKeys(
const bool *keys) {
1676 const bool escapeDown = keys[SDL_SCANCODE_ESCAPE];
1677 const bool hardDropDown = keys[SDL_SCANCODE_Z];
1678 const bool rotateDown = keys[SDL_SCANCODE_UP];
1679 const bool resetDown = keys[SDL_SCANCODE_R];
1680 const bool enterDown = keys[SDL_SCANCODE_RETURN];
1682 if (escapeDown && !escapeHeld) {
1685 if (gameOver && enterDown && !enterHeld) {
1686 restartIntroSequence();
1687 escapeHeld = escapeDown;
1688 hardDropHeld = hardDropDown;
1689 rotateHeld = rotateDown;
1690 resetHeld = resetDown;
1691 enterHeld = enterDown;
1694 if (hardDropDown && !hardDropHeld) {
1697 if (rotateDown && !rotateHeld) {
1700 if (resetDown && !resetHeld) {
1704 escapeHeld = escapeDown;
1705 hardDropHeld = hardDropDown;
1706 rotateHeld = rotateDown;
1707 resetHeld = resetDown;
1708 enterHeld = enterDown;
1711 void handleGameOverKeys(
const bool *keys) {
1712 const bool enterDown = keys[SDL_SCANCODE_RETURN];
1713 const bool escapeDown = keys[SDL_SCANCODE_ESCAPE];
1714 const bool resetDown = keys[SDL_SCANCODE_R];
1715 const bool highScoresDown = keys[SDL_SCANCODE_H];
1716 const bool backspaceDown = keys[SDL_SCANCODE_BACKSPACE];
1719 if (backspaceDown && !backspaceHeld && !playerName.empty()) {
1720 playerName.pop_back();
1722 if (enterDown && !enterHeld) {
1724 backspaceHeld = backspaceDown;
1725 enterHeld = enterDown;
1726 escapeHeld = escapeDown;
1727 resetHeld = resetDown;
1728 menuEnterHeld = highScoresDown;
1732 backspaceHeld = backspaceDown;
1733 enterHeld = enterDown;
1734 escapeHeld = escapeDown;
1735 resetHeld = resetDown;
1736 menuEnterHeld = highScoresDown;
1739 if (enterDown && !enterHeld) {
1740 restartIntroSequence();
1741 backspaceHeld = backspaceDown;
1742 enterHeld = enterDown;
1743 escapeHeld = escapeDown;
1744 resetHeld = resetDown;
1745 menuEnterHeld = highScoresDown;
1750 if (resetDown && !resetHeld) {
1751 restartIntroSequence();
1753 if (highScoresDown && !menuEnterHeld) {
1757 if (escapeDown && !escapeHeld) {
1761 backspaceHeld = backspaceDown;
1762 enterHeld = enterDown;
1763 escapeHeld = escapeDown;
1764 resetHeld = resetDown;
1765 menuEnterHeld = highScoresDown;
1768 [[nodiscard]]
bool collides(
int pieceX,
int pieceY,
const std::array<Block, 4> &blocks)
const {
1769 for (
const Block &block : blocks) {
1770 const int x = pieceX + block.x;
1771 const int y = pieceY + block.y;
1772 if (x < 0 || x >= boardWidth || y < 0) {
1775 if (y >= boardHeight) {
1778 if (board[y][x].color >= 0) {
1785 void movePiece(
int dx,
int dy) {
1786 if (!gameOver && !lineClearActive && !collides(active.x + dx, active.y + dy, active.blocks)) {
1793 if (gameOver || lineClearActive) {
1796 if (collides(active.x, active.y - 1, active.blocks)) {
1804 if (gameOver || lineClearActive) {
1807 while (!collides(active.x, active.y - 1, active.blocks)) {
1813 void rotatePiece() {
1814 if (gameOver || lineClearActive) {
1818 if (!collides(active.x, active.y, rotated)) {
1819 active.blocks = rotated;
1822 if (!collides(active.x - 1, active.y, rotated)) {
1824 active.blocks = rotated;
1827 if (!collides(active.x + 1, active.y, rotated)) {
1829 active.blocks = rotated;
1841 if (lineClearActive) {
1842 const auto now = std::chrono::steady_clock::now();
1843 const float elapsed = std::chrono::duration<float>(now - lineClearStart).count();
1844 if (elapsed >= 1.0f) {
1845 finalizeLineClear();
1850 const auto now = std::chrono::steady_clock::now();
1851 const float elapsed = std::chrono::duration<float>(now - lastFall).count();
1852 if (elapsed >= fallSeconds) {
1859 for (
const Block &block : active.blocks) {
1860 const int x = active.x + block.x;
1861 const int y = active.y + block.y;
1862 if (x >= 0 && x < boardWidth && y >= 0 && y < boardHeight) {
1863 board[y][x].color = active.color;
1864 LockedBlock locked{};
1865 locked.block = loadBlockModel(active.color);
1868 lockedBlocks.push_back(std::move(locked));
1871 const auto fullRows = findFullRows();
1872 if (std::any_of(fullRows.begin(), fullRows.end(), [](
bool value) { return value; })) {
1873 startLineClear(fullRows);
1877 lastFall = std::chrono::steady_clock::now();
1880 [[nodiscard]] std::array<bool, boardHeight> findFullRows()
const {
1881 std::array<bool, boardHeight> fullRows{};
1885 full = full && board[y][x].color >= 0;
1892 void startLineClear(
const std::array<bool, boardHeight> &fullRows) {
1893 clearingRows = fullRows;
1894 lineClearActive =
true;
1895 lineClearStart = std::chrono::steady_clock::now();
1898 void finalizeLineClear() {
1900 for (
bool row : clearingRows) {
1907 for (
int readY = 0; readY <
boardHeight; ++readY) {
1908 if (clearingRows[readY]) {
1911 if (writeY != readY) {
1912 board[writeY] = board[readY];
1917 for (Cell &cell : board[writeY]) {
1922 eraseClearedModels(clearingRows);
1923 clearingRows.fill(
false);
1924 lineClearActive =
false;
1926 score += cleared * 10;
1930 linesCleared += cleared;
1934 lastFall = std::chrono::steady_clock::now();
1937 void eraseClearedModels(
const std::array<bool, boardHeight> &fullRows) {
1938 std::vector<LockedBlock> remaining{};
1939 remaining.reserve(lockedBlocks.size());
1940 for (LockedBlock &locked : lockedBlocks) {
1941 if (locked.y >= 0 && locked.y < boardHeight && fullRows[locked.y]) {
1942 if (locked.block.model) {
1943 locked.block.model->cleanup(
this);
1949 for (
int y = 0; y < locked.y; ++y) {
1954 locked.y -= rowsBelow;
1955 remaining.push_back(std::move(locked));
1957 lockedBlocks = std::move(remaining);
1960 [[nodiscard]]
bool isClearingRow(
int y)
const {
1961 return lineClearActive && y >= 0 && y <
boardHeight && clearingRows[y];
1964 [[nodiscard]]
bool isLineClearVisible()
const {
1965 const auto now = std::chrono::steady_clock::now();
1966 const float elapsed = std::chrono::duration<float>(now - lineClearStart).count();
1967 constexpr float blinkIntervalSeconds = 0.12f;
1968 return (
static_cast<int>(elapsed / blinkIntervalSeconds) % 2) == 0;
1971 void drawIntroOverlay(VkCommandBuffer cmd,
const VkExtent2D &extent) {
1976 const auto now = std::chrono::steady_clock::now();
1977 const float elapsed = std::chrono::duration<float>(now - introStart).count();
1978 const float fadeProgress = std::clamp((elapsed - introHoldSeconds) / introFadeSeconds, 0.0f, 1.0f);
1979 const float alpha = 1.0f - fadeProgress;
1981 introSprite->setShaderParams(elapsed, 0.0f, 0.0f, alpha);
1982 introSprite->drawSpriteRect(0, 0,
static_cast<int>(extent.width),
static_cast<int>(extent.height));
1983 vkCmdBindPipeline(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS,
sprite_pipeline);
1985 introSprite->clearQueue();
1988 void drawGameOverOverlay(VkCommandBuffer cmd,
const VkExtent2D &extent) {
1994 const float alpha = gameOverFadeAlpha();
1995 gameOverSprite->setShaderParams(0.0f, 0.0f, 0.0f, alpha);
1996 gameOverSprite->drawSpriteRect(0, 0,
static_cast<int>(extent.width),
static_cast<int>(extent.height));
1997 vkCmdBindPipeline(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS,
sprite_pipeline);
1999 gameOverSprite->clearQueue();
2002 [[nodiscard]] glm::vec3 blockPosition(
int x,
int y)
const {
2003 const float worldX = (
static_cast<float>(x) - (
static_cast<float>(
boardWidth) - 1.0f) * 0.5f) * cubeSpacing;
2004 const float worldY = (
static_cast<float>(y) - (
static_cast<float>(
boardHeight) - 1.0f) * 0.5f) * cubeSpacing;
2005 return glm::vec3(worldX, worldY, 0.0f);
2008 [[nodiscard]] glm::mat4 blockMatrix(
int x,
int y,
float scale = cubeScale)
const {
2009 glm::mat4 model(1.0f);
2010 model = glm::translate(model, blockPosition(x, y));
2011 model = glm::scale(model, glm::vec3(scale));
2015 void drawBlock(VkCommandBuffer cmd,
2016 uint32_t imageIndex,
2021 const glm::mat4 &view,
2022 const glm::mat4 &proj) {
2023 mxvk::UniformBufferObject ubo{};
2024 ubo.
model = blockMatrix(x, y);
2027 ubo.
fx = glm::vec4(colorTints[color], 1.0f);
2028 block.model->updateUBO(imageIndex, ubo);
2029 block.model->render(cmd, imageIndex,
false);
2032 void drawFrame(VkCommandBuffer cmd, uint32_t imageIndex,
const glm::mat4 &view,
const glm::mat4 &proj) {
2035 drawFrameBlock(cmd, imageIndex, frameModels[index++], -1, y, view, proj);
2036 drawFrameBlock(cmd, imageIndex, frameModels[index++], boardWidth, y, view, proj);
2039 drawFrameBlock(cmd, imageIndex, frameModels[index++], x, -1, view, proj);
2043 void drawFrameBlock(VkCommandBuffer cmd,
2044 uint32_t imageIndex,
2048 const glm::mat4 &view,
2049 const glm::mat4 &proj) {
2050 mxvk::UniformBufferObject ubo{};
2051 ubo.
model = blockMatrix(x, y, cubeScale * 0.82f);
2054 ubo.
fx = glm::vec4(colorTints[7], 1.0f);
2055 block.model->updateUBO(imageIndex, ubo);
2056 block.model->render(cmd, imageIndex,
false);
2059 void drawSpriteRect(mxvk::VK_Sprite *sprite, VkCommandBuffer cmd,
const VkExtent2D &extent,
int x,
int y,
int w,
int h) {
2060 if (sprite ==
nullptr) {
2064 vkCmdBindPipeline(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS,
sprite_pipeline);
2069 void drawNextPiecePreview(VkCommandBuffer cmd, [[maybe_unused]] uint32_t imageIndex,
const VkExtent2D &extent) {
2074 const int panelSize = std::min({220,
static_cast<int>(
static_cast<float>(extent.width) * 0.28f),
static_cast<int>(
static_cast<float>(extent.height) * 0.34f)});
2075 const int panelW = panelSize;
2076 const int panelH = panelSize;
2077 const int margin = 24;
2078 const int panelX =
static_cast<int>(extent.width) - panelW - margin;
2079 const int panelY = 88;
2080 const int border = 4;
2082 drawSpriteRect(previewBorderSprite, cmd, extent, panelX, panelY, panelW, border);
2083 drawSpriteRect(previewBorderSprite, cmd, extent, panelX, panelY + panelH - border, panelW, border);
2084 drawSpriteRect(previewBorderSprite, cmd, extent, panelX, panelY, border, panelH);
2085 drawSpriteRect(previewBorderSprite, cmd, extent, panelX + panelW - border, panelY, border, panelH);
2087 printText(
"Next", panelX + 12, panelY - 28, SDL_Color{255, 255, 255, 255});
2089 int minX = nextPiece.blocks[0].x;
2090 int maxX = nextPiece.blocks[0].x;
2091 int minY = nextPiece.blocks[0].y;
2092 int maxY = nextPiece.blocks[0].y;
2093 for (
const Block &block : nextPiece.blocks) {
2094 minX = std::min(minX, block.x);
2095 maxX = std::max(maxX, block.x);
2096 minY = std::min(minY, block.y);
2097 maxY = std::max(maxY, block.y);
2100 const float innerPadding = 28.0f;
2101 const float innerSize =
static_cast<float>(panelSize) - innerPadding * 2.0f;
2102 const int blockSize =
static_cast<int>(std::min(34.0f, innerSize /
static_cast<float>(std::max(maxX - minX + 1, maxY - minY + 1))));
2103 const float pieceW =
static_cast<float>(maxX - minX + 1) * blockSize;
2104 const float pieceH =
static_cast<float>(maxY - minY + 1) * blockSize;
2105 const float centerX =
static_cast<float>(panelX) +
static_cast<float>(panelW) * 0.5f;
2106 const float centerY =
static_cast<float>(panelY) +
static_cast<float>(panelH) * 0.5f;
2107 const float originX = centerX - pieceW * 0.5f;
2108 const float originY = centerY - pieceH * 0.5f;
2109 mxvk::VK_Sprite *blockSprite = blockPreviewSprites[
static_cast<std::size_t
>(nextPiece.color)];
2111 for (std::size_t i = 0; i < nextPiece.blocks.size(); ++i) {
2112 const Block &pieceBlock = nextPiece.blocks[i];
2113 const float x = originX +
static_cast<float>(pieceBlock.x - minX) * blockSize;
2114 const float y = originY +
static_cast<float>(pieceBlock.y - minY) * blockSize;
2115 drawSpriteRect(blockSprite, cmd, extent,
static_cast<int>(x),
static_cast<int>(y), blockSize, blockSize);
2119 void drawOpponentGrid(VkCommandBuffer cmd,
const VkExtent2D &extent) {
2124 const int margin = 24;
2125 const int previewSize = std::min({220,
static_cast<int>(
static_cast<float>(extent.width) * 0.28f),
static_cast<int>(
static_cast<float>(extent.height) * 0.34f)});
2126 const int previewBottom = 88 + previewSize;
2127 const int panelTop = previewBottom + 56;
2128 const int textAreaHeight = 78;
2129 const int panelBottom =
static_cast<int>(extent.height) - margin - textAreaHeight;
2130 const int availableHeight = panelBottom - panelTop;
2131 const int availableWidth = std::min(260,
static_cast<int>(
static_cast<float>(extent.width) * 0.24f));
2132 if (availableWidth < 70 || availableHeight < 120) {
2135 const int blockSize = std::max(4, std::min(availableWidth / boardWidth, availableHeight / boardHeight));
2138 const int panelX =
static_cast<int>(extent.width) - gridW - margin;
2139 const int panelY = panelTop + std::max(0, (availableHeight - gridH) / 2);
2140 const int border = std::max(2, blockSize / 5);
2141 const auto textXInsideRightEdge = [
this, &extent, margin, panelX](
const std::initializer_list<std::string> &lines) {
2143 for (
const std::string &line : lines) {
2147 maxWidth = std::max(maxWidth, width);
2150 return std::max(margin, std::min(panelX,
static_cast<int>(extent.width) - margin - maxWidth));
2153 drawSpriteRect(previewBorderSprite, cmd, extent, panelX - border, panelY - border, gridW + border * 2, border);
2154 drawSpriteRect(previewBorderSprite, cmd, extent, panelX - border, panelY + gridH, gridW + border * 2, border);
2155 drawSpriteRect(previewBorderSprite, cmd, extent, panelX - border, panelY - border, border, gridH + border * 2);
2156 drawSpriteRect(previewBorderSprite, cmd, extent, panelX + gridW, panelY - border, border, gridH + border * 2);
2158 printText(
"Opponent", panelX, panelY - 30, SDL_Color{255, 255, 255, 255});
2159 if (!opponentSnapshot.hasState) {
2160 const std::string waitingText =
"Waiting...";
2161 printText(waitingText, textXInsideRightEdge({waitingText}), panelY + gridH + 12, SDL_Color{255, 220, 120, 255});
2167 const int color = opponentSnapshot.cells[y][x];
2168 if (color < 0 || color >=
static_cast<int>(blockPreviewSprites.size())) {
2171 const int screenX = panelX + x * blockSize;
2172 const int screenY = panelY + (
boardHeight - 1 - y) * blockSize;
2173 drawSpriteRect(blockPreviewSprites[
static_cast<std::size_t
>(color)], cmd, extent, screenX, screenY, blockSize, blockSize);
2177 if (!opponentSnapshot.gameOver) {
2178 for (
const Block &block : opponentSnapshot.activeBlocks) {
2179 const int x = opponentSnapshot.activeX + block.x;
2180 const int y = opponentSnapshot.activeY + block.y;
2181 if (x < 0 || x >= boardWidth || y < 0 || y >= boardHeight) {
2184 const int screenX = panelX + x * blockSize;
2185 const int screenY = panelY + (
boardHeight - 1 - y) * blockSize;
2186 drawSpriteRect(blockPreviewSprites[
static_cast<std::size_t
>(opponentSnapshot.activeColor)], cmd, extent, screenX, screenY, blockSize, blockSize);
2190 const std::string scoreText = std::format(
"Score {}", opponentSnapshot.score);
2191 const std::string linesText = std::format(
"Lines {} Lv {}", opponentSnapshot.lines, opponentSnapshot.level);
2192 const std::string gameOverText =
"Game Over";
2193 const int statsX = textXInsideRightEdge({scoreText, linesText, gameOverText});
2194 printText(scoreText, statsX, panelY + gridH + 12, SDL_Color{255, 255, 255, 255});
2195 printText(linesText, statsX, panelY + gridH + 38, SDL_Color{180, 220, 255, 255});
2196 if (opponentSnapshot.gameOver) {
2197 printText(gameOverText, statsX, panelY + gridH + 64, SDL_Color{255, 120, 120, 255});
2201 void drawCreditsModel(VkCommandBuffer cmd, uint32_t imageIndex,
const VkExtent2D &extent) {
2202 if (creditsTuxModel ==
nullptr) {
2206 const float aspect = (extent.height > 0U) ?
static_cast<float>(extent.width) /
static_cast<float>(extent.height) : 1.0f;
2207 glm::mat4 view = glm::lookAt(glm::vec3(0.0f, 0.25f, 2.8f), glm::vec3(0.0f, 0.05f, 0.0f), glm::vec3(0.0f, 1.0f, 0.0f));
2208 view = glm::rotate(view, glm::radians(-10.0f), glm::vec3(1.0f, 0.0f, 0.0f));
2209 glm::mat4 proj = glm::perspective(glm::radians(40.0f), aspect, 0.1f, 100.0f);
2210 proj[1][1] *= -1.0f;
2212 const float elapsed =
static_cast<float>(SDL_GetTicks()) / 1000.0f;
2213 glm::mat4 model(1.0f);
2214 model = glm::translate(model, glm::vec3(0.0f, -0.34f, 0.0f));
2215 model = glm::rotate(model, elapsed * 0.75f, glm::vec3(0.0f, 1.0f, 0.0f));
2216 model = glm::rotate(model, std::sin(elapsed * 0.6f) * 0.08f, glm::vec3(1.0f, 0.0f, 0.0f));
2217 model = glm::scale(model, glm::vec3(0.228f));
2219 mxvk::UniformBufferObject ubo{};
2223 ubo.
fx = glm::vec4(1.0f, 1.0f, 1.0f, elapsed);
2224 creditsTuxModel->updateUBO(imageIndex, ubo);
2225 creditsTuxModel->render(cmd, imageIndex,
false);
2233 const int centerX =
static_cast<int>(extent.width) / 2;
2234 const float alpha = screenFadeAlpha();
2235 const auto withAlpha = [alpha](SDL_Color color) {
2236 color.a =
static_cast<Uint8
>(
static_cast<float>(color.a) * alpha);
2242 const int titleY =
static_cast<int>(
static_cast<float>(extent.height) * 0.18f);
2243 const int menuY =
static_cast<int>(
static_cast<float>(extent.height) * 0.40f);
2244 const int spacing =
static_cast<int>(std::max(34.0f,
static_cast<float>(extent.height) * 0.07f));
2245 printCenteredText(
"MXVK 3D Tetris", centerX, titleY, withAlpha(SDL_Color{255, 255, 0, 255}));
2246 printCenteredText(
"Choose a mode", centerX, titleY + 42, withAlpha(SDL_Color{255, 255, 255, 255}));
2248 const char *items[] = {
"New Game",
"Network Multiplayer",
"High Scores",
"Credits"};
2249 for (
int i = 0; i < 4; ++i) {
2250 const SDL_Color color = (i == cursorPos) ? SDL_Color{255, 255, 0, 255} : SDL_Color{255, 255, 255, 255};
2251 printCenteredText(items[i], centerX, menuY + i * spacing, withAlpha(color));
2253 printCenteredText(
"Use arrows and Enter", centerX,
static_cast<int>(
static_cast<float>(extent.height) * 0.86f), withAlpha(SDL_Color{180, 180, 180, 255}));
2257 const int baseY =
static_cast<int>(
static_cast<float>(extent.height) * 0.16f);
2258 printCenteredText(
"High Scores", centerX, baseY, withAlpha(SDL_Color{255, 255, 0, 255}));
2259 const auto &scores = highScores.entries();
2260 const int listLeftX =
static_cast<int>(
static_cast<float>(extent.width) * 0.34f);
2261 const int listRightX =
static_cast<int>(
static_cast<float>(extent.width) * 0.70f);
2262 const int lineHeight = 30;
2263 for (std::size_t i = 0; i < 10U; ++i) {
2264 const int rowY = baseY + 54 +
static_cast<int>(i) * lineHeight;
2265 const bool hasScore = i < scores.size();
2266 const std::string nameText = hasScore ? scores[i].name :
"---";
2267 const std::string scoreText = hasScore ? std::format(
"{}", scores[i].score) :
"---";
2268 printText(std::format(
"{:>2}. {}", i + 1U, nameText),
2271 withAlpha(SDL_Color{255, 255, 255, 255}));
2273 int scoreHeight = 0;
2275 printText(scoreText, listRightX - scoreWidth, rowY, withAlpha(SDL_Color{255, 255, 255, 255}));
2277 printText(scoreText, listRightX, rowY, withAlpha(SDL_Color{255, 255, 255, 255}));
2280 printCenteredText(
"Press Enter or Escape to return", centerX,
static_cast<int>(
static_cast<float>(extent.height) * 0.82f), withAlpha(SDL_Color{200, 200, 200, 255}));
2284 const int baseY =
static_cast<int>(
static_cast<float>(extent.height) * 0.2f);
2285 printCenteredText(
"Credits", centerX, baseY, withAlpha(SDL_Color{255, 255, 0, 255}));
2286 printCenteredText(
"MXVK 3D Tetris", centerX, baseY + 48, withAlpha(SDL_Color{255, 255, 255, 255}));
2287 printCenteredText(
"Built with Vulkan and SDL3", centerX, baseY + 84, withAlpha(SDL_Color{255, 220, 120, 255}));
2288 printCenteredText(
"Press Enter or Escape to return", centerX,
static_cast<int>(
static_cast<float>(extent.height) * 0.82f), withAlpha(SDL_Color{200, 200, 200, 255}));
2292 const int baseY =
static_cast<int>(
static_cast<float>(extent.height) * 0.16f);
2293 printCenteredText(
"Network Multiplayer", centerX, baseY, withAlpha(SDL_Color{255, 255, 0, 255}));
2294 printCenteredText(std::format(
"Port: {}", multiplayerPort), centerX, baseY + 42, withAlpha(SDL_Color{255, 220, 120, 255}));
2295 printCenteredText(std::format(
"Peer IP: {}", multiplayerHost.empty() ?
"_" : multiplayerHost), centerX, baseY + 86, withAlpha(SDL_Color{255, 255, 255, 255}));
2296 printCenteredText(multiplayerStatus, centerX, baseY + 126, withAlpha(SDL_Color{180, 220, 255, 255}));
2297 printCenteredText(
"H host J/Enter join Backspace edit", centerX, baseY + 178, withAlpha(SDL_Color{255, 255, 255, 255}));
2298 printCenteredText(
"Run one copy as host, then connect from the other machine by IP.", centerX, baseY + 216, withAlpha(SDL_Color{200, 200, 200, 255}));
2299 printCenteredText(
"Escape returns to menu", centerX,
static_cast<int>(
static_cast<float>(extent.height) * 0.82f), withAlpha(SDL_Color{200, 200, 200, 255}));
2303 const int baseY =
static_cast<int>(
static_cast<float>(extent.height) * 0.20f);
2304 const float gameOverAlpha = gameOverFadeAlpha();
2305 const auto withGameOverAlpha = [gameOverAlpha](SDL_Color color) {
2306 color.a =
static_cast<Uint8
>(
static_cast<float>(color.a) * gameOverAlpha);
2310 printCenteredText(
"Game Over", centerX, baseY, withGameOverAlpha(SDL_Color{255, 255, 0, 255}));
2311 printCenteredText(std::format(
"Final Score: {}", score), centerX, baseY + 42, withGameOverAlpha(SDL_Color{255, 255, 255, 255}));
2312 if (!multiplayerResult.empty()) {
2313 printCenteredText(multiplayerResult, centerX, baseY + 82, withGameOverAlpha(SDL_Color{255, 220, 120, 255}));
2314 printCenteredText(
"Enter to restart", centerX, baseY + 124, withGameOverAlpha(SDL_Color{200, 200, 200, 255}));
2315 printCenteredText(
"Escape for menu", centerX, baseY + 158, withGameOverAlpha(SDL_Color{200, 200, 200, 255}));
2319 printCenteredText(
"New high score", centerX, baseY + 82, withGameOverAlpha(SDL_Color{255, 220, 120, 255}));
2320 printCenteredText(std::format(
"Name: {}", playerName.empty() ?
"_" : playerName),
2323 withGameOverAlpha(SDL_Color{255, 255, 255, 255}));
2324 printCenteredText(std::format(
"Pick: [{}]", currentNameCharacter()), centerX, baseY + 154, withGameOverAlpha(SDL_Color{120, 255, 255, 255}));
2325 printCenteredText(
"Up/Down choose, A/Space add", centerX, baseY + 190, withGameOverAlpha(SDL_Color{200, 200, 200, 255}));
2326 printCenteredText(
"X/Enter save, B/Backspace delete", centerX, baseY + 224, withGameOverAlpha(SDL_Color{200, 200, 200, 255}));
2328 printCenteredText(
"R to restart from intro", centerX, baseY + 92, withGameOverAlpha(SDL_Color{255, 220, 120, 255}));
2329 printCenteredText(
"H for high scores", centerX, baseY + 126, withGameOverAlpha(SDL_Color{200, 200, 200, 255}));
2330 printCenteredText(
"Enter to restart", centerX, baseY + 160, withGameOverAlpha(SDL_Color{200, 200, 200, 255}));
2335 printText(std::format(
"Score: {}", score), 15, 15, withAlpha(SDL_Color{255, 255, 255, 255}));
2336 printText(std::format(
"Lines Cleared: {}", linesCleared), 15, 45, withAlpha(SDL_Color{255, 220, 120, 255}));
2337 printText(std::format(
"Level: {}", level), 15, 75, withAlpha(SDL_Color{120, 220, 255, 255}));
2338 if (multiplayerActive) {
2339 printText(multiplayerHostSide ?
"Multiplayer: Host" :
"Multiplayer: Guest", 15, 105, withAlpha(SDL_Color{180, 255, 180, 255}));
2342 const int baseY =
static_cast<int>(
static_cast<float>(extent.height) * 0.58f);
2343 const float gameOverAlpha = gameOverFadeAlpha();
2344 const auto withGameOverAlpha = [gameOverAlpha](SDL_Color color) {
2345 color.a =
static_cast<Uint8
>(
static_cast<float>(color.a) * gameOverAlpha);
2348 printCenteredText(std::format(
"Final Score: {}", score), centerX, baseY, withGameOverAlpha(SDL_Color{255, 255, 255, 255}));
2349 printCenteredText(multiplayerResult.empty() ?
"Game over" : multiplayerResult, centerX, baseY + 40, withGameOverAlpha(SDL_Color{255, 220, 120, 255}));
2357 void printCenteredText(
const std::string &text,
int centerX,
int y,
const SDL_Color &color) {
2361 printText(text, centerX - textWidth / 2, y, color);
2367 void triggerBackgroundTransition() {
2368 backgroundTransitionActive =
true;
2369 backgroundTransitionStart = std::chrono::steady_clock::now();
2372 void updateBackgroundTransitionState() {
2373 if (!backgroundTransitionActive) {
2376 const auto now = std::chrono::steady_clock::now();
2377 const float elapsed = std::chrono::duration<float>(now - backgroundTransitionStart).count();
2378 if (elapsed >= backgroundTransitionSeconds) {
2379 backgroundTransitionActive =
false;
2383 [[nodiscard]] mxvk::VK_Sprite *screenSpriteFor(AppScreen screen)
const {
2386 return menuBackgroundSprite;
2388 return highScoresBackgroundSprite;
2390 return creditsBackgroundSprite;
2392 return multiplayerBackgroundSprite;
2401 void drawFadedSprite(mxvk::VK_Sprite *sprite, VkCommandBuffer cmd,
const VkExtent2D &extent,
float alpha) {
2406 sprite->
drawSpriteRect(0, 0,
static_cast<int>(extent.width),
static_cast<int>(extent.height));
2407 vkCmdBindPipeline(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS,
sprite_pipeline);
2412 void drawGameScreenTransitionOverlay(VkCommandBuffer cmd,
const VkExtent2D &extent) {
2416 drawFadedSprite(screenSpriteFor(transitionFromScreen), cmd, extent, 1.0f - screenFadeAlpha());
2419 void drawScreenBackdrop(VkCommandBuffer cmd,
const VkExtent2D &extent) {
2424 updateBackgroundTransitionState();
2426 mxvk::VK_Sprite *sprite = background;
2427 if (backgroundTransitionActive && backgroundTransitionSprite !=
nullptr) {
2428 sprite = backgroundTransitionSprite;
2430 if (sprite ==
nullptr) {
2434 if (sprite == backgroundTransitionSprite) {
2435 const auto now = std::chrono::steady_clock::now();
2436 const float elapsed = std::chrono::duration<float>(now - backgroundTransitionStart).count();
2440 sprite->
drawSpriteRect(0, 0,
static_cast<int>(extent.width),
static_cast<int>(extent.height));
2441 vkCmdBindPipeline(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS,
sprite_pipeline);
2447 const float alpha = screenFadeAlpha();
2448 if (screenTransitionActive) {
2449 drawFadedSprite(screenSpriteFor(transitionFromScreen), cmd, extent, 1.0f - alpha);
2451 drawFadedSprite(screenSpriteFor(screen), cmd, extent, alpha);