291 TetrisWindow(
const std::string &path,
int width,
int height,
bool fullscreen,
bool enable_vsync) :
mxvk::
VK_Window(
"-[ MXVK 3D Tetris ]-", width, height, fullscreen,
MXVK_VALIDATION, enable_vsync), assetRoot((path.empty() || path ==
".") ? std::string(
tetris_ASSET_DIR) : path), dataRoot(assetRoot +
"/data"), shaderRoot(dataRoot), highScores(
resolveScoreFilePath()) {
292 std::random_device rd;
294 setFont(dataRoot +
"/font.ttf", 22);
296 tryOpenFirstGamepad();
297#if defined(MXVK_WITH_MIXER) || defined(WITH_MIXER)
298 music = std::make_unique<mxvk::VK_Mixer>();
299 musicTrack = music->loadMusic(dataRoot +
"/music.ogg");
300 ensureMusicPlaying();
302 background =
createSprite(dataRoot +
"/psychedelic_background.png");
303 backgroundTransitionSprite =
createSprite(dataRoot +
"/psychedelic_background.png", shaderRoot +
"/sprite.vert.spv", shaderRoot +
"/tetris_background_transition.frag.spv");
304 menuBackgroundSprite =
createSprite(dataRoot +
"/start_screen.png", shaderRoot +
"/sprite.vert.spv", shaderRoot +
"/tetris_screen_fade.frag.spv");
305 highScoresBackgroundSprite =
createSprite(dataRoot +
"/high_scores_screen.png", shaderRoot +
"/sprite.vert.spv", shaderRoot +
"/tetris_screen_fade.frag.spv");
306 creditsBackgroundSprite =
createSprite(dataRoot +
"/credits_screen.png", shaderRoot +
"/sprite.vert.spv", shaderRoot +
"/tetris_screen_fade.frag.spv");
307 multiplayerBackgroundSprite =
createSprite(dataRoot +
"/multiplayer_screen.png", shaderRoot +
"/sprite.vert.spv", shaderRoot +
"/tetris_screen_fade.frag.spv");
309 const uint32_t whitePixel = 0xFFFFFFFFu;
310 previewBorderSprite->updateTexture(&whitePixel, 1, 1);
311 for (std::size_t i = 0; i < blockPreviewSprites.size(); ++i) {
314 gameOverSprite =
createSprite(dataRoot +
"/gameover.png");
315 introSprite =
createSprite(dataRoot +
"/intro.png", shaderRoot +
"/tetris_intro.vert.spv", shaderRoot +
"/tetris_intro.frag.spv");
316 introStart = std::chrono::steady_clock::now();
323 if (
device != VK_NULL_HANDLE) {
326#if defined(MXVK_WITH_MIXER) || defined(WITH_MIXER)
332 closeMultiplayerSession();
342 if (e.type == SDL_EVENT_QUIT) {
347 if (e.type == SDL_EVENT_KEY_DOWN) {
352 handleNameEntryKey(e.key.key);
357 if (e.type == SDL_EVENT_GAMEPAD_ADDED) {
358 if (!openGamepad(e.gdevice.which)) {
359 tryOpenFirstGamepad();
364 if (e.type == SDL_EVENT_GAMEPAD_REMOVED) {
365 if (gamepad !=
nullptr && e.gdevice.which == gamepadId) {
367 tryOpenFirstGamepad();
372 if (e.type == SDL_EVENT_GAMEPAD_BUTTON_DOWN) {
374 handleNameEntryButton(e.gbutton.button);
377 handleGamepadButtonDown(e.gbutton.button);
382 ensureMusicPlaying();
383 tryOpenFirstGamepad();
385 updateScreenTransition();
386 updateMultiplayerNetwork();
396 drawScreenBackdrop(cmd, extent);
399 const float aspect = (extent.height > 0U) ?
static_cast<float>(extent.width) /
static_cast<float>(extent.height) : 1.0f;
400 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));
401 view = glm::rotate(view, glm::radians(gridPitch), glm::vec3(1.0f, 0.0f, 0.0f));
402 view = glm::rotate(view, glm::radians(gridYaw), glm::vec3(0.0f, 1.0f, 0.0f));
403 view = glm::rotate(view, glm::radians(gridRoll), glm::vec3(0.0f, 0.0f, 1.0f));
405 glm::mat4 proj = glm::perspective(glm::radians(46.0f), aspect, 0.1f, 100.0f);
407 const bool blinkVisible = !lineClearActive || isLineClearVisible();
410 if (blinkVisible || !isClearingRow(locked.y)) {
411 drawBlock(cmd, imageIndex, locked.block, locked.x, locked.y, locked.block.color, view, proj);
415 if (!lineClearActive) {
416 for (
size_t i = 0; i < activeModels.size(); ++i) {
417 const int x = active.x + active.blocks[i].x;
418 const int y = active.y + active.blocks[i].y;
419 drawBlock(cmd, imageIndex, activeModels[i], x, y, active.color, view, proj);
423 drawFrame(cmd, imageIndex, view, proj);
424 drawNextPiecePreview(cmd, imageIndex, extent);
425 drawOpponentGrid(cmd, extent);
428 drawCreditsModel(cmd, imageIndex, extent);
430 drawGameScreenTransitionOverlay(cmd, extent);
431 drawIntroOverlay(cmd, extent);
432 drawGameOverOverlay(cmd, extent);
436 std::string assetRoot;
437 std::string dataRoot;
438 std::string shaderRoot;
439 std::array<std::array<Cell, boardWidth>,
boardHeight> board{};
442 std::vector<LockedBlock> lockedBlocks{};
443 std::array<BlockModel, 4> activeModels{};
444 std::vector<BlockModel> frameModels{};
445 mxvk::VK_Sprite *background =
nullptr;
446 mxvk::VK_Sprite *backgroundTransitionSprite =
nullptr;
447 mxvk::VK_Sprite *menuBackgroundSprite =
nullptr;
448 mxvk::VK_Sprite *highScoresBackgroundSprite =
nullptr;
449 mxvk::VK_Sprite *creditsBackgroundSprite =
nullptr;
450 mxvk::VK_Sprite *multiplayerBackgroundSprite =
nullptr;
451 mxvk::VK_Sprite *previewBorderSprite =
nullptr;
452 std::array<mxvk::VK_Sprite *, 8> blockPreviewSprites{};
453 mxvk::VK_Sprite *introSprite =
nullptr;
454 mxvk::VK_Sprite *gameOverSprite =
nullptr;
455 std::unique_ptr<mxvk::VKAbstractModel> creditsTuxModel{};
457#if defined(MXVK_WITH_MIXER) || defined(WITH_MIXER)
458 std::unique_ptr<mxvk::VK_Mixer> music{};
461 SDL_Gamepad *gamepad =
nullptr;
462 SDL_JoystickID gamepadId = 0;
463 std::chrono::steady_clock::time_point introStart{std::chrono::steady_clock::now()};
464 std::chrono::steady_clock::time_point lastFall{std::chrono::steady_clock::now()};
465 std::chrono::steady_clock::time_point lastInputUpdate{std::chrono::steady_clock::now()};
466 std::chrono::steady_clock::time_point lineClearStart{std::chrono::steady_clock::now()};
467 std::chrono::steady_clock::time_point backgroundTransitionStart{std::chrono::steady_clock::now()};
468 std::chrono::steady_clock::time_point screenTransitionStart{std::chrono::steady_clock::now()};
469 std::chrono::steady_clock::time_point gameOverTransitionStart{std::chrono::steady_clock::now()};
470 bool introActive =
true;
471 float gridYaw = 0.0f;
472 float gridPitch = 0.0f;
473 float gridRoll = 0.0f;
474 float cameraDistance = 2.45f;
476 int linesCleared = 0;
479 float fallSeconds = 0.65f;
480 float moveRepeatTimer = 0.0f;
481 float softDropRepeatTimer = 0.0f;
482 float gamepadMoveRepeatTimer = 0.0f;
483 float gamepadSoftDropRepeatTimer = 0.0f;
484 bool gameOver =
false;
485 bool enteringName =
false;
486 bool highScoresAfterSave =
false;
487 std::size_t nameCharIndex = 0;
488 std::string playerName{};
489 bool hardDropHeld =
false;
490 bool rotateHeld =
false;
491 bool resetHeld =
false;
492 bool enterHeld =
false;
493 bool escapeHeld =
false;
494 bool backspaceHeld =
false;
495 bool menuUpHeld =
false;
496 bool menuDownHeld =
false;
497 bool menuEnterHeld =
false;
498 bool introSkipHeld =
false;
499 bool introFadeStarted =
false;
500 bool screenTransitionActive =
false;
501 bool gameOverTransitionActive =
false;
502 bool lineClearActive =
false;
503 bool backgroundTransitionActive =
false;
504 std::array<bool, boardHeight> clearingRows{};
505 PieceQueueEntry nextPiece{};
508 mxnetwork::MXNetworkInit networkInit{};
509 mxnetwork::Socket listenSocket{};
510 mxnetwork::Socket peerSocket{};
511 MultiplayerSnapshot opponentSnapshot{};
513 std::string multiplayerHost{
"127.0.0.1"};
514 std::string multiplayerStatus{
"Enter host IP, then press J. Press H to host."};
515 std::string multiplayerResult{};
516 std::string networkReceiveBuffer{};
517 std::chrono::steady_clock::time_point lastNetworkSend{std::chrono::steady_clock::now()};
518 std::uint32_t networkSequence = 0;
519 int multiplayerCursor = 0;
520 bool multiplayerActive =
false;
521 bool multiplayerHostSide =
false;
522 bool hostHeld =
false;
523 bool joinHeld =
false;
524 static constexpr float introHoldSeconds = 5.0f;
525 static constexpr float introFadeSeconds = 1.0f;
526 static constexpr float screenTransitionSeconds = 0.36f;
527 static constexpr float gameOverTransitionSeconds = 0.5f;
528 static constexpr float backgroundTransitionSeconds = 1.25f;
529 static constexpr Sint16 gamepadDeadzone = 10000;
530 static constexpr float gamepadMoveInitialDelaySeconds = 0.22f;
531 static constexpr float gamepadMoveRepeatSeconds = 0.12f;
532 static constexpr float gamepadSoftDropInitialDelaySeconds = 0.18f;
533 static constexpr float gamepadSoftDropRepeatSeconds = 0.08f;
534 static constexpr float gamepadStickRotateSpeed = 120.0f;
535 static constexpr float gamepadStickPitchSpeed = 100.0f;
536 static constexpr float gamepadStickScale = 1.0f / 32768.0f;
537 int gamepadMoveDirection = 0;
538 float gamepadMoveHeldSeconds = 0.0f;
539 bool gamepadSoftDropHeld =
false;
542 frameModels.reserve(boardWidth + (boardHeight * 2) + 2);
544 frameModels.push_back(loadBlockModel(7));
548 [[nodiscard]] BlockModel loadBlockModel(
int color) {
551 block.model = std::make_unique<mxvk::VKAbstractModel>();
552 block.model->load(
this, dataRoot +
"/cube.mxmod.z", dataRoot +
"/" + textureManifests[color], dataRoot, 1.0f);
553 block.model->setShaders(
this, shaderRoot +
"/tetris_piece.vert.spv", shaderRoot +
"/tetris_piece.frag.spv");
557 void cleanupModels() {
558 forEachModel([
this](mxvk::VKAbstractModel &model) { model.
cleanup(
this); });
559 lockedBlocks.clear();
562 template <
typename Fn>
void forEachModel(Fn fn) {
563 for (LockedBlock &locked : lockedBlocks) {
564 if (locked.block.model) {
565 fn(*locked.block.model);
568 for (BlockModel &block : activeModels) {
573 for (BlockModel &block : frameModels) {
578 if (creditsTuxModel) {
579 fn(*creditsTuxModel);
583 void initCreditsModel() {
584 creditsTuxModel = std::make_unique<mxvk::VKAbstractModel>();
585 creditsTuxModel->load(
this, dataRoot +
"/tux/tux.obj", dataRoot +
"/tux/tux.mtl", dataRoot +
"/tux", 0.35f);
586 creditsTuxModel->setShaders(
this, shaderRoot +
"/tetris_model.vert.spv", shaderRoot +
"/tetris_model.frag.spv");
592 for (
auto &row : board) {
593 for (Cell &cell : row) {
598 gameOverTransitionActive =
false;
599 multiplayerResult.clear();
600 lineClearActive =
false;
601 backgroundTransitionActive =
false;
602 clearingRows.fill(
false);
606 lastFall = std::chrono::steady_clock::now();
607 nextPiece = randomPiece();
611 void resetOpponentSnapshot() {
612 opponentSnapshot = MultiplayerSnapshot{};
613 networkReceiveBuffer.clear();
616 void closeMultiplayerSession(std::string status =
"Enter host IP, then press J. Press H to host.") {
617 if (peerSocket.valid()) {
620 if (listenSocket.valid()) {
621 listenSocket.close();
623 multiplayerActive =
false;
624 multiplayerHostSide =
false;
626 multiplayerStatus = std::move(status);
627 resetOpponentSnapshot();
630 [[nodiscard]] std::string multiplayerOutcomeText()
const {
631 if (gameOver && !opponentSnapshot.gameOver) {
632 return "You lost. Opponent won.";
634 if (!gameOver && opponentSnapshot.gameOver) {
635 return "You won. Opponent lost.";
637 if (score > opponentSnapshot.score) {
638 return "You won by score.";
640 if (score < opponentSnapshot.score) {
641 return "You lost by score.";
646 void enterMultiplayerGameOver(std::string reason) {
647 const std::string outcome = multiplayerOutcomeText();
648 closeMultiplayerSession(std::move(reason));
649 multiplayerResult = outcome;
652 gameOverTransitionActive =
true;
653 gameOverTransitionStart = std::chrono::steady_clock::now();
654 enteringName =
false;
655 highScoresAfterSave =
false;
656 resetMenuLatchState();
659 void finishLocalMultiplayerGameOver() {
660 multiplayerResult =
"You lost. Opponent won.";
661 sendMultiplayerSnapshot(
true);
662 closeMultiplayerSession(
"Game over.");
663 enteringName =
false;
664 highScoresAfterSave =
false;
667 void beginHosting() {
668 closeMultiplayerSession();
670 if (!listenSocket.listen(multiplayerPort, 1)) {
672 multiplayerStatus = std::format(
"Could not host on port {}", multiplayerPort);
675 listenSocket.setblocking(
false);
677 multiplayerHostSide =
true;
678 multiplayerStatus = std::format(
"Hosting on port {}. Waiting for peer...", multiplayerPort);
682 closeMultiplayerSession();
683 if (multiplayerHost.empty()) {
685 multiplayerStatus =
"Enter an IP address before joining.";
690 multiplayerStatus = std::format(
"Connecting to {}:{}...", multiplayerHost, multiplayerPort);
691 if (!peerSocket.connect(multiplayerHost, multiplayerPort)) {
694 multiplayerStatus = std::format(
"Could not connect to {}:{}", multiplayerHost, multiplayerPort);
697 peerSocket.setblocking(
false);
698 multiplayerHostSide =
false;
699 finishMultiplayerConnection(
"Connected as guest.");
702 void finishMultiplayerConnection(
const std::string &status) {
703 if (listenSocket.valid()) {
704 listenSocket.close();
707 multiplayerStatus = status;
708 multiplayerActive =
true;
709 resetOpponentSnapshot();
713 screenTransitionActive =
false;
715 lastInputUpdate = std::chrono::steady_clock::now();
716 lastNetworkSend = std::chrono::steady_clock::now();
717 resetMenuLatchState();
718 sendMultiplayerSnapshot(
true);
721 void updateMultiplayerNetwork() {
724 std::optional<mxnetwork::Socket> accepted = listenSocket.accept();
726 peerSocket = std::move(*accepted);
727 peerSocket.setblocking(
false);
728 finishMultiplayerConnection(
"Peer connected. Multiplayer started.");
730 }
catch (
const mxnetwork::Exception &ex) {
732 multiplayerStatus = std::format(
"Accept failed: {}", ex.
text());
740 receiveMultiplayerData();
742 const auto now = std::chrono::steady_clock::now();
743 const float elapsed = std::chrono::duration<float>(now - lastNetworkSend).count();
744 if (elapsed >= 0.05f) {
745 sendMultiplayerSnapshot(
false);
746 lastNetworkSend = now;
750 std::string makeMultiplayerSnapshotLine() {
751 std::ostringstream out;
752 out <<
"S " << networkSequence++ <<
' ' << score <<
' ' << linesCleared <<
' ' << level <<
' ' << (gameOver ? 1 : 0) <<
' ';
755 const int color = board[y][x].color;
756 out << ((color >= 0 && color <= 7) ? static_cast<char>(
'0' + color) :
'.');
759 out <<
' ' << active.color <<
' ' << active.x <<
' ' << active.y;
760 for (
const Block &block : active.blocks) {
761 out <<
' ' << block.x <<
' ' << block.y;
767 void sendMultiplayerSnapshot(
bool force) {
774 const std::string line = makeMultiplayerSnapshotLine();
775 const ssize_t written = peerSocket.write(line.data(), line.size(), 0);
777 enterMultiplayerGameOver(
"Peer disconnected.");
781 void receiveMultiplayerData() {
782 std::array<char, 2048> buffer{};
783 while (peerSocket.valid()) {
784 const ssize_t received = peerSocket.read(buffer.data(), buffer.size(), 0);
786 networkReceiveBuffer.append(buffer.data(),
static_cast<std::size_t
>(received));
787 consumeMultiplayerLines();
791 enterMultiplayerGameOver(
"Peer disconnected.");
797 void consumeMultiplayerLines() {
798 std::size_t newline = networkReceiveBuffer.find(
'\n');
799 while (newline != std::string::npos) {
800 const std::string line = networkReceiveBuffer.substr(0, newline);
801 networkReceiveBuffer.erase(0, newline + 1);
802 parseMultiplayerSnapshot(line);
803 newline = networkReceiveBuffer.find(
'\n');
805 constexpr std::size_t maxBufferedBytes = 8192;
806 if (networkReceiveBuffer.size() > maxBufferedBytes) {
807 networkReceiveBuffer.clear();
811 void parseMultiplayerSnapshot(
const std::string &line) {
812 std::istringstream in(line);
814 [[maybe_unused]] std::uint32_t sequence = 0;
816 int remoteGameOver = 0;
817 MultiplayerSnapshot snapshot{};
819 in >> type >> sequence >> snapshot.score >> snapshot.lines >> snapshot.level >> remoteGameOver >> cells;
820 if (type !=
'S' || cells.size() !=
static_cast<std::size_t
>(boardWidth * boardHeight)) {
824 snapshot.gameOver = remoteGameOver != 0;
827 const char ch = cells[
static_cast<std::size_t
>(y *
boardWidth + x)];
828 snapshot.cells[y][x] = (ch >=
'0' && ch <=
'7') ? ch -
'0' : -1;
832 in >> snapshot.activeColor >> snapshot.activeX >> snapshot.activeY;
833 for (Block &block : snapshot.activeBlocks) {
834 in >> block.x >> block.y;
839 snapshot.activeColor = std::clamp(snapshot.activeColor, 0, 7);
840 snapshot.hasState =
true;
841 opponentSnapshot = snapshot;
844 void clearLockedBlocks() {
845 for (LockedBlock &locked : lockedBlocks) {
846 if (locked.block.model) {
847 locked.block.model->cleanup(
this);
850 lockedBlocks.clear();
854 active.blocks = nextPiece.blocks;
857 active.color = nextPiece.color;
858 reloadActiveModels(active.color);
859 gameOver = collides(active.x, active.y, active.blocks);
861 enterGameOverState();
863 nextPiece = randomPiece();
866 void reloadActiveModels(
int color) {
867 for (BlockModel &block : activeModels) {
869 block.model->cleanup(
this);
871 block = loadBlockModel(color);
875 [[nodiscard]] PieceQueueEntry randomPiece() {
876 std::uniform_int_distribution<int> dist(0,
static_cast<int>(
pieceDefinitions.size()) - 1);
878 return PieceQueueEntry{definition.blocks, definition.color};
881 void updateDifficulty() {
882 const int previousLevel = level;
883 level = (linesCleared / 8) + 1;
884 static constexpr std::array<float, 16> arcadeFallSeconds{
903 const std::size_t index = std::min<std::size_t>(arcadeFallSeconds.size() - 1,
static_cast<std::size_t
>(std::max(level - 1, 0)));
904 fallSeconds = arcadeFallSeconds[index];
905 if (level > previousLevel) {
906 triggerBackgroundTransition();
910#if defined(MXVK_WITH_MIXER) || defined(WITH_MIXER)
911 void ensureMusicPlaying() {
915 if (musicTrack < 0) {
918 if (!music->isMusicPlaying(musicTrack)) {
919 if (music->playMusic(musicTrack, -1) != 0) {
920 throw mxvk::Exception(
"Could not start Tetris background music");
925 void ensureMusicPlaying() {}
929 const auto now = std::chrono::steady_clock::now();
930 float deltaSeconds = std::chrono::duration<float>(now - lastInputUpdate).count();
931 lastInputUpdate = now;
932 deltaSeconds = std::clamp(deltaSeconds, 0.0f, 0.05f);
934 const bool *keys = SDL_GetKeyboardState(
nullptr);
935 if (keys ==
nullptr) {
940 const bool enterDown = keys[SDL_SCANCODE_RETURN];
941 const bool spaceDown = keys[SDL_SCANCODE_SPACE];
942 const bool escapeDown = keys[SDL_SCANCODE_ESCAPE];
943 const bool skipDown = enterDown || spaceDown;
944 if (skipDown && !introSkipHeld) {
945 const auto now = std::chrono::steady_clock::now();
946 introStart = now - std::chrono::duration_cast<std::chrono::steady_clock::duration>(std::chrono::duration<float>(introHoldSeconds));
947 introFadeStarted =
false;
950 if (escapeDown && !escapeHeld) {
953 introSkipHeld = skipDown;
954 escapeHeld = escapeDown;
958 if (screenTransitionActive) {
966 handleGameOverKeys(keys);
971 handleMultiplayerKeys(keys);
976 handleMenuKeys(keys);
980 handleHeldViewRotation(keys, deltaSeconds);
981 handleHeldPieceMovement(keys, deltaSeconds);
982 handleOneShotKeys(keys);
983 handleGamepadInput(deltaSeconds);
986 void handleMenuKeys(
const bool *keys) {
987 if (screenTransitionActive) {
988 menuUpHeld = keys[SDL_SCANCODE_UP];
989 menuDownHeld = keys[SDL_SCANCODE_DOWN];
990 menuEnterHeld = keys[SDL_SCANCODE_RETURN];
991 escapeHeld = keys[SDL_SCANCODE_ESCAPE];
995 const bool upDown = keys[SDL_SCANCODE_UP];
996 const bool downDown = keys[SDL_SCANCODE_DOWN];
997 const bool enterDown = keys[SDL_SCANCODE_RETURN];
998 const bool escapeDown = keys[SDL_SCANCODE_ESCAPE];
1001 if (upDown && !menuUpHeld) {
1002 cursorPos = (cursorPos + 3) % 4;
1004 if (downDown && !menuDownHeld) {
1005 cursorPos = (cursorPos + 1) % 4;
1008 if (enterDown && !menuEnterHeld) {
1010 if (cursorPos == 0) {
1012 }
else if (cursorPos == 1) {
1013 goToNetworkMultiplayer();
1014 }
else if (cursorPos == 2) {
1016 }
else if (cursorPos == 3) {
1020 highScoresAfterSave =
false;
1021 restartIntroSequence();
1026 if (escapeDown && !escapeHeld) {
1030 highScoresAfterSave =
false;
1035 menuUpHeld = upDown;
1036 menuDownHeld = downDown;
1037 menuEnterHeld = enterDown;
1038 escapeHeld = escapeDown;
1041 void handleMultiplayerKeys(
const bool *keys) {
1042 const bool escapeDown = keys[SDL_SCANCODE_ESCAPE];
1043 const bool enterDown = keys[SDL_SCANCODE_RETURN];
1044 const bool hostDown = keys[SDL_SCANCODE_H];
1045 const bool joinDown = keys[SDL_SCANCODE_J];
1046 const bool backspaceDown = keys[SDL_SCANCODE_BACKSPACE];
1048 if (escapeDown && !escapeHeld) {
1049 closeMultiplayerSession();
1052 if (hostDown && !hostHeld) {
1055 if (joinDown && !joinHeld) {
1058 if (enterDown && !menuEnterHeld) {
1061 if (backspaceDown && !backspaceHeld && !multiplayerHost.empty()) {
1062 multiplayerHost.pop_back();
1065 appendMultiplayerAddressCharacters(keys);
1067 escapeHeld = escapeDown;
1068 menuEnterHeld = enterDown;
1069 hostHeld = hostDown;
1070 joinHeld = joinDown;
1071 backspaceHeld = backspaceDown;
1074 void appendMultiplayerAddressCharacters(
const bool *keys) {
1075 static constexpr std::array<SDL_Scancode, 10> digitKeys{{
1087 static constexpr std::array<SDL_Scancode, 10> keypadDigitKeys{{
1100 auto appendChar = [
this](
char ch) {
1101 constexpr std::size_t maxAddressLength = 64;
1102 if (multiplayerHost.size() < maxAddressLength) {
1103 multiplayerHost.push_back(ch);
1107 for (
int i = 0; i < 10; ++i) {
1108 if ((keys[digitKeys[
static_cast<std::size_t
>(i)]] || keys[keypadDigitKeys[
static_cast<std::size_t
>(i)]]) && multiplayerCursor != (i + 1)) {
1109 appendChar(
static_cast<char>(
'0' + i));
1110 multiplayerCursor = i + 1;
1114 if ((keys[SDL_SCANCODE_PERIOD] || keys[SDL_SCANCODE_KP_PERIOD]) && multiplayerCursor != 11) {
1116 multiplayerCursor = 11;
1119 if (keys[SDL_SCANCODE_MINUS] && multiplayerCursor != 12) {
1121 multiplayerCursor = 12;
1124 if (!keys[SDL_SCANCODE_PERIOD] && !keys[SDL_SCANCODE_KP_PERIOD] && !keys[SDL_SCANCODE_MINUS]) {
1125 bool anyDigitDown =
false;
1126 for (
int i = 0; i < 10; ++i) {
1127 anyDigitDown = anyDigitDown || keys[digitKeys[
static_cast<std::size_t
>(i)]] || keys[keypadDigitKeys[
static_cast<std::size_t
>(i)]];
1129 if (!anyDigitDown) {
1130 multiplayerCursor = 0;
1135 void handleGamepadInput(
float deltaSeconds) {
1137 gamepadMoveDirection = 0;
1138 gamepadMoveHeldSeconds = 0.0f;
1139 gamepadSoftDropHeld =
false;
1140 gamepadMoveRepeatTimer = 0.0f;
1141 gamepadSoftDropRepeatTimer = 0.0f;
1145 const Sint16 leftX = SDL_GetGamepadAxis(gamepad, SDL_GAMEPAD_AXIS_LEFTX);
1146 const Sint16 leftY = SDL_GetGamepadAxis(gamepad, SDL_GAMEPAD_AXIS_LEFTY);
1147 const Sint16 rightX = SDL_GetGamepadAxis(gamepad, SDL_GAMEPAD_AXIS_RIGHTX);
1148 const Sint16 rightY = SDL_GetGamepadAxis(gamepad, SDL_GAMEPAD_AXIS_RIGHTY);
1150 const int moveDirection = (leftX < -gamepadDeadzone) ? -1 : (leftX > gamepadDeadzone) ? 1 : 0;
1151 if (moveDirection == 0) {
1152 gamepadMoveDirection = 0;
1153 gamepadMoveHeldSeconds = 0.0f;
1154 gamepadMoveRepeatTimer = 0.0f;
1156 if (moveDirection != gamepadMoveDirection) {
1157 gamepadMoveDirection = moveDirection;
1158 gamepadMoveHeldSeconds = 0.0f;
1159 gamepadMoveRepeatTimer = 0.0f;
1160 movePiece(gamepadMoveDirection, 0);
1162 gamepadMoveHeldSeconds += deltaSeconds;
1163 const float threshold = (gamepadMoveHeldSeconds < gamepadMoveInitialDelaySeconds) ? gamepadMoveInitialDelaySeconds : gamepadMoveRepeatSeconds;
1164 gamepadMoveRepeatTimer += deltaSeconds;
1165 if (gamepadMoveRepeatTimer >= threshold) {
1166 movePiece(gamepadMoveDirection, 0);
1167 gamepadMoveRepeatTimer = 0.0f;
1172 const bool softDropDown = leftY > gamepadDeadzone;
1173 if (!softDropDown) {
1174 gamepadSoftDropHeld =
false;
1175 gamepadSoftDropRepeatTimer = 0.0f;
1177 const float threshold = gamepadSoftDropHeld ? gamepadSoftDropRepeatSeconds : gamepadSoftDropInitialDelaySeconds;
1178 gamepadSoftDropRepeatTimer += deltaSeconds;
1179 if (gamepadSoftDropRepeatTimer >= threshold) {
1181 gamepadSoftDropRepeatTimer = 0.0f;
1182 lastFall = std::chrono::steady_clock::now();
1183 gamepadSoftDropHeld =
true;
1187 if (std::abs(rightX) > gamepadDeadzone) {
1188 gridYaw +=
static_cast<float>(rightX) * gamepadStickScale * gamepadStickRotateSpeed * deltaSeconds;
1190 if (std::abs(rightY) > gamepadDeadzone) {
1191 gridPitch = std::clamp(gridPitch -
static_cast<float>(rightY) * gamepadStickScale * gamepadStickPitchSpeed * deltaSeconds, -70.0f, 70.0f);
1194 constexpr float gamepadZoomSpeed = 3.2f;
1195 if (SDL_GetGamepadButton(gamepad, SDL_GAMEPAD_BUTTON_LEFT_SHOULDER)) {
1196 cameraDistance = std::min(9.0f, cameraDistance + gamepadZoomSpeed * deltaSeconds);
1198 if (SDL_GetGamepadButton(gamepad, SDL_GAMEPAD_BUTTON_RIGHT_SHOULDER)) {
1199 cameraDistance = std::max(1.65f, cameraDistance - gamepadZoomSpeed * deltaSeconds);
1203 void handleGamepadButtonDown(Uint8 button) {
1204 if (gamepad ==
nullptr) {
1207 if (screenTransitionActive) {
1212 if (button == SDL_GAMEPAD_BUTTON_SOUTH || button == SDL_GAMEPAD_BUTTON_START) {
1213 introActive =
false;
1221 if (button == SDL_GAMEPAD_BUTTON_DPAD_UP) {
1222 cursorPos = (cursorPos + 3) % 4;
1223 }
else if (button == SDL_GAMEPAD_BUTTON_DPAD_DOWN) {
1224 cursorPos = (cursorPos + 1) % 4;
1225 }
else if (button == SDL_GAMEPAD_BUTTON_SOUTH || button == SDL_GAMEPAD_BUTTON_START) {
1226 if (cursorPos == 0) {
1228 }
else if (cursorPos == 1) {
1229 goToNetworkMultiplayer();
1230 }
else if (cursorPos == 2) {
1232 }
else if (cursorPos == 3) {
1235 }
else if (button == SDL_GAMEPAD_BUTTON_BACK || button == SDL_GAMEPAD_BUTTON_EAST) {
1241 if (button == SDL_GAMEPAD_BUTTON_START || button == SDL_GAMEPAD_BUTTON_SOUTH) {
1242 restartIntroSequence();
1243 }
else if (button == SDL_GAMEPAD_BUTTON_BACK || button == SDL_GAMEPAD_BUTTON_EAST) {
1248 if (lineClearActive) {
1251 if (button == SDL_GAMEPAD_BUTTON_SOUTH) {
1253 }
else if (button == SDL_GAMEPAD_BUTTON_EAST) {
1255 }
else if (button == SDL_GAMEPAD_BUTTON_BACK) {
1260 if (button == SDL_GAMEPAD_BUTTON_SOUTH || button == SDL_GAMEPAD_BUTTON_START) {
1264 restartIntroSequence();
1266 }
else if (button == SDL_GAMEPAD_BUTTON_BACK || button == SDL_GAMEPAD_BUTTON_EAST) {
1267 highScoresAfterSave =
false;
1274 if (button == SDL_GAMEPAD_BUTTON_SOUTH || button == SDL_GAMEPAD_BUTTON_START || button == SDL_GAMEPAD_BUTTON_BACK || button == SDL_GAMEPAD_BUTTON_EAST) {
1275 highScoresAfterSave =
false;
1284 bool openGamepad(SDL_JoystickID
id) {
1285 if (gamepad !=
nullptr && gamepadId ==
id) {
1289 gamepad = SDL_OpenGamepad(
id);
1290 if (gamepad ==
nullptr) {
1297 void closeGamepad() {
1298 if (gamepad !=
nullptr) {
1299 SDL_CloseGamepad(gamepad);
1305 void tryOpenFirstGamepad() {
1306 if (gamepad !=
nullptr) {
1310 SDL_JoystickID *ids = SDL_GetGamepads(&count);
1311 if (ids ==
nullptr || count <= 0) {
1312 if (ids !=
nullptr) {
1317 openGamepad(ids[0]);
1321 void updateIntroState() {
1326 const auto now = std::chrono::steady_clock::now();
1327 const float elapsed = std::chrono::duration<float>(now - introStart).count();
1328 if (!introFadeStarted && elapsed >= introHoldSeconds) {
1329 introFadeStarted =
true;
1332 if (elapsed >= (introHoldSeconds + introFadeSeconds)) {
1333 introActive =
false;
1334 introFadeStarted =
false;
1340 const AppScreen previousScreen = screen;
1341 closeMultiplayerSession();
1343 introActive =
false;
1346 transitionFromScreen = previousScreen;
1347 screenTransitionStart = std::chrono::steady_clock::now();
1348 lastInputUpdate = std::chrono::steady_clock::now();
1349 resetMenuLatchState();
1352 void restartIntroSequence() {
1354 highScoresAfterSave =
false;
1356 introFadeStarted =
false;
1357 introStart = std::chrono::steady_clock::now();
1359 screenTransitionActive =
false;
1361 lastInputUpdate = std::chrono::steady_clock::now();
1362 resetMenuLatchState();
1363 introSkipHeld =
true;
1366 void enterGameOverState() {
1368 gameOverTransitionActive =
true;
1369 gameOverTransitionStart = std::chrono::steady_clock::now();
1372 if (multiplayerActive) {
1373 finishLocalMultiplayerGameOver();
1374 resetMenuLatchState();
1377 enteringName = highScores.qualifies(score);
1378 highScoresAfterSave =
false;
1379 resetMenuLatchState();
1382 void stopNameEntry() {
1383 enteringName =
false;
1388 void commitScoreEntry() {
1389 highScores.addScore(playerName, score);
1391 highScoresAfterSave =
true;
1392 menuEnterHeld =
true;
1398 void cycleNameCharacter(
int delta) {
1399 if (!enteringName) {
1403 const int next = (
static_cast<int>(nameCharIndex) + delta + count) % count;
1404 nameCharIndex =
static_cast<std::size_t
>(next);
1407 void appendCurrentNameCharacter() {
1408 if (!enteringName || playerName.size() >= nameMaxLength) {
1411 playerName += currentNameCharacter();
1414 void deleteNameCharacter() {
1415 if (!enteringName || playerName.empty()) {
1418 playerName.pop_back();
1421 void handleNameEntryKey(SDL_Keycode key) {
1422 if (key == SDLK_UP) {
1423 cycleNameCharacter(-1);
1424 }
else if (key == SDLK_DOWN) {
1425 cycleNameCharacter(1);
1426 }
else if (key == SDLK_SPACE) {
1427 appendCurrentNameCharacter();
1428 }
else if (key == SDLK_BACKSPACE) {
1429 deleteNameCharacter();
1430 }
else if (key == SDLK_RETURN) {
1432 }
else if (key == SDLK_ESCAPE) {
1433 highScoresAfterSave =
false;
1438 void handleNameEntryButton(Uint8 button) {
1439 if (button == SDL_GAMEPAD_BUTTON_DPAD_UP) {
1440 cycleNameCharacter(-1);
1441 }
else if (button == SDL_GAMEPAD_BUTTON_DPAD_DOWN) {
1442 cycleNameCharacter(1);
1443 }
else if (button == SDL_GAMEPAD_BUTTON_SOUTH) {
1444 appendCurrentNameCharacter();
1445 }
else if (button == SDL_GAMEPAD_BUTTON_WEST) {
1447 }
else if (button == SDL_GAMEPAD_BUTTON_EAST || button == SDL_GAMEPAD_BUTTON_BACK) {
1448 deleteNameCharacter();
1457 closeMultiplayerSession();
1459 gameOverTransitionActive =
false;
1460 highScoresAfterSave =
false;
1466 void goToHighScores() {
1476 void requestScreen(AppScreen nextScreen) {
1477 if (screen == nextScreen && !screenTransitionActive) {
1481 screenTransitionActive =
false;
1482 screen = nextScreen;
1486 screenTransitionActive =
false;
1487 screen = nextScreen;
1490 transitionFromScreen = screen;
1491 screen = nextScreen;
1492 screenTransitionActive =
true;
1493 screenTransitionStart = std::chrono::steady_clock::now();
1494 resetMenuLatchState();
1497 void updateScreenTransition() {
1498 if (!screenTransitionActive) {
1501 const auto now = std::chrono::steady_clock::now();
1502 const float elapsed = std::chrono::duration<float>(now - screenTransitionStart).count();
1503 if (elapsed >= screenTransitionSeconds) {
1504 screenTransitionActive =
false;
1505 transitionFromScreen = screen;
1506 resetMenuLatchState();
1510 float screenFadeAlpha()
const {
1511 if (!screenTransitionActive) {
1514 const auto now = std::chrono::steady_clock::now();
1515 const float elapsed = std::chrono::duration<float>(now - screenTransitionStart).count();
1516 return std::clamp(elapsed / screenTransitionSeconds, 0.0f, 1.0f);
1519 float gameOverFadeAlpha()
const {
1520 if (!gameOverTransitionActive) {
1523 const auto now = std::chrono::steady_clock::now();
1524 const float elapsed = std::chrono::duration<float>(now - gameOverTransitionStart).count();
1525 return std::clamp(elapsed / gameOverTransitionSeconds, 0.0f, 1.0f);
1528 void resetMenuLatchState() {
1530 menuDownHeld =
false;
1531 menuEnterHeld =
false;
1532 introSkipHeld =
false;
1533 backspaceHeld =
false;
1536 multiplayerCursor = 0;
1539 void updateMenuInputLatchState() {
1540 const bool *keys = SDL_GetKeyboardState(
nullptr);
1541 if (keys ==
nullptr) {
1544 menuUpHeld = keys[SDL_SCANCODE_UP];
1545 menuDownHeld = keys[SDL_SCANCODE_DOWN];
1546 menuEnterHeld = keys[SDL_SCANCODE_RETURN];
1547 escapeHeld = keys[SDL_SCANCODE_ESCAPE];
1550 void handleHeldViewRotation(
const bool *keys,
float deltaSeconds) {
1551 constexpr float yawSpeed = 115.0f;
1552 constexpr float pitchSpeed = 90.0f;
1553 constexpr float rollSpeed = 100.0f;
1554 constexpr float zoomSpeed = 3.2f;
1556 if (keys[SDL_SCANCODE_A]) {
1557 gridYaw -= yawSpeed * deltaSeconds;
1559 if (keys[SDL_SCANCODE_D]) {
1560 gridYaw += yawSpeed * deltaSeconds;
1562 if (keys[SDL_SCANCODE_W]) {
1563 gridPitch = std::clamp(gridPitch + pitchSpeed * deltaSeconds, -70.0f, 70.0f);
1565 if (keys[SDL_SCANCODE_S]) {
1566 gridPitch = std::clamp(gridPitch - pitchSpeed * deltaSeconds, -70.0f, 70.0f);
1568 if (keys[SDL_SCANCODE_Q]) {
1569 gridRoll -= rollSpeed * deltaSeconds;
1571 if (keys[SDL_SCANCODE_E]) {
1572 gridRoll += rollSpeed * deltaSeconds;
1574 if (keys[SDL_SCANCODE_PAGEUP]) {
1575 cameraDistance = std::max(1.65f, cameraDistance - zoomSpeed * deltaSeconds);
1577 if (keys[SDL_SCANCODE_PAGEDOWN]) {
1578 cameraDistance = std::min(9.0f, cameraDistance + zoomSpeed * deltaSeconds);
1582 void handleHeldPieceMovement(
const bool *keys,
float deltaSeconds) {
1583 if (gameOver || lineClearActive) {
1584 moveRepeatTimer = 0.0f;
1585 softDropRepeatTimer = 0.0f;
1589 constexpr float horizontalRepeatSeconds = 0.14f;
1590 constexpr float softDropRepeatSeconds = 0.075f;
1592 moveRepeatTimer += deltaSeconds;
1593 softDropRepeatTimer += deltaSeconds;
1595 int horizontalDirection = 0;
1596 if (keys[SDL_SCANCODE_LEFT] && !keys[SDL_SCANCODE_RIGHT]) {
1597 horizontalDirection = -1;
1598 }
else if (keys[SDL_SCANCODE_RIGHT] && !keys[SDL_SCANCODE_LEFT]) {
1599 horizontalDirection = 1;
1602 if (horizontalDirection != 0) {
1603 if (moveRepeatTimer >= horizontalRepeatSeconds) {
1604 movePiece(horizontalDirection, 0);
1605 moveRepeatTimer = 0.0f;
1608 moveRepeatTimer = horizontalRepeatSeconds;
1611 if (keys[SDL_SCANCODE_DOWN]) {
1612 if (softDropRepeatTimer >= softDropRepeatSeconds) {
1614 softDropRepeatTimer = 0.0f;
1615 lastFall = std::chrono::steady_clock::now();
1618 softDropRepeatTimer = softDropRepeatSeconds;
1622 void handleOneShotKeys(
const bool *keys) {
1623 const bool escapeDown = keys[SDL_SCANCODE_ESCAPE];
1624 const bool hardDropDown = keys[SDL_SCANCODE_Z];
1625 const bool rotateDown = keys[SDL_SCANCODE_UP];
1626 const bool resetDown = keys[SDL_SCANCODE_R];
1627 const bool enterDown = keys[SDL_SCANCODE_RETURN];
1629 if (escapeDown && !escapeHeld) {
1632 if (gameOver && enterDown && !enterHeld) {
1633 restartIntroSequence();
1634 escapeHeld = escapeDown;
1635 hardDropHeld = hardDropDown;
1636 rotateHeld = rotateDown;
1637 resetHeld = resetDown;
1638 enterHeld = enterDown;
1641 if (hardDropDown && !hardDropHeld) {
1644 if (rotateDown && !rotateHeld) {
1647 if (resetDown && !resetHeld) {
1651 escapeHeld = escapeDown;
1652 hardDropHeld = hardDropDown;
1653 rotateHeld = rotateDown;
1654 resetHeld = resetDown;
1655 enterHeld = enterDown;
1658 void handleGameOverKeys(
const bool *keys) {
1659 const bool enterDown = keys[SDL_SCANCODE_RETURN];
1660 const bool escapeDown = keys[SDL_SCANCODE_ESCAPE];
1661 const bool resetDown = keys[SDL_SCANCODE_R];
1662 const bool highScoresDown = keys[SDL_SCANCODE_H];
1663 const bool backspaceDown = keys[SDL_SCANCODE_BACKSPACE];
1666 if (backspaceDown && !backspaceHeld && !playerName.empty()) {
1667 playerName.pop_back();
1669 if (enterDown && !enterHeld) {
1671 backspaceHeld = backspaceDown;
1672 enterHeld = enterDown;
1673 escapeHeld = escapeDown;
1674 resetHeld = resetDown;
1675 menuEnterHeld = highScoresDown;
1679 backspaceHeld = backspaceDown;
1680 enterHeld = enterDown;
1681 escapeHeld = escapeDown;
1682 resetHeld = resetDown;
1683 menuEnterHeld = highScoresDown;
1686 if (enterDown && !enterHeld) {
1687 restartIntroSequence();
1688 backspaceHeld = backspaceDown;
1689 enterHeld = enterDown;
1690 escapeHeld = escapeDown;
1691 resetHeld = resetDown;
1692 menuEnterHeld = highScoresDown;
1697 if (resetDown && !resetHeld) {
1698 restartIntroSequence();
1700 if (highScoresDown && !menuEnterHeld) {
1704 if (escapeDown && !escapeHeld) {
1708 backspaceHeld = backspaceDown;
1709 enterHeld = enterDown;
1710 escapeHeld = escapeDown;
1711 resetHeld = resetDown;
1712 menuEnterHeld = highScoresDown;
1715 [[nodiscard]]
bool collides(
int pieceX,
int pieceY,
const std::array<Block, 4> &blocks)
const {
1716 for (
const Block &block : blocks) {
1717 const int x = pieceX + block.x;
1718 const int y = pieceY + block.y;
1719 if (x < 0 || x >= boardWidth || y < 0) {
1722 if (y >= boardHeight) {
1725 if (board[y][x].color >= 0) {
1732 void movePiece(
int dx,
int dy) {
1733 if (!gameOver && !lineClearActive && !collides(active.x + dx, active.y + dy, active.blocks)) {
1740 if (gameOver || lineClearActive) {
1743 if (collides(active.x, active.y - 1, active.blocks)) {
1751 if (gameOver || lineClearActive) {
1754 while (!collides(active.x, active.y - 1, active.blocks)) {
1760 void rotatePiece() {
1761 if (gameOver || lineClearActive) {
1765 if (!collides(active.x, active.y, rotated)) {
1766 active.blocks = rotated;
1769 if (!collides(active.x - 1, active.y, rotated)) {
1771 active.blocks = rotated;
1774 if (!collides(active.x + 1, active.y, rotated)) {
1776 active.blocks = rotated;
1788 if (lineClearActive) {
1789 const auto now = std::chrono::steady_clock::now();
1790 const float elapsed = std::chrono::duration<float>(now - lineClearStart).count();
1791 if (elapsed >= 1.0f) {
1792 finalizeLineClear();
1797 const auto now = std::chrono::steady_clock::now();
1798 const float elapsed = std::chrono::duration<float>(now - lastFall).count();
1799 if (elapsed >= fallSeconds) {
1806 for (
const Block &block : active.blocks) {
1807 const int x = active.x + block.x;
1808 const int y = active.y + block.y;
1809 if (x >= 0 && x < boardWidth && y >= 0 && y < boardHeight) {
1810 board[y][x].color = active.color;
1811 LockedBlock locked{};
1812 locked.block = loadBlockModel(active.color);
1815 lockedBlocks.push_back(std::move(locked));
1818 const auto fullRows = findFullRows();
1819 if (std::any_of(fullRows.begin(), fullRows.end(), [](
bool value) { return value; })) {
1820 startLineClear(fullRows);
1824 lastFall = std::chrono::steady_clock::now();
1827 [[nodiscard]] std::array<bool, boardHeight> findFullRows()
const {
1828 std::array<bool, boardHeight> fullRows{};
1832 full = full && board[y][x].color >= 0;
1839 void startLineClear(
const std::array<bool, boardHeight> &fullRows) {
1840 clearingRows = fullRows;
1841 lineClearActive =
true;
1842 lineClearStart = std::chrono::steady_clock::now();
1845 void finalizeLineClear() {
1847 for (
bool row : clearingRows) {
1854 for (
int readY = 0; readY <
boardHeight; ++readY) {
1855 if (clearingRows[readY]) {
1858 if (writeY != readY) {
1859 board[writeY] = board[readY];
1864 for (Cell &cell : board[writeY]) {
1869 eraseClearedModels(clearingRows);
1870 clearingRows.fill(
false);
1871 lineClearActive =
false;
1873 score += cleared * 10;
1877 linesCleared += cleared;
1881 lastFall = std::chrono::steady_clock::now();
1884 void eraseClearedModels(
const std::array<bool, boardHeight> &fullRows) {
1885 std::vector<LockedBlock> remaining{};
1886 remaining.reserve(lockedBlocks.size());
1887 for (LockedBlock &locked : lockedBlocks) {
1888 if (locked.y >= 0 && locked.y < boardHeight && fullRows[locked.y]) {
1889 if (locked.block.model) {
1890 locked.block.model->cleanup(
this);
1896 for (
int y = 0; y < locked.y; ++y) {
1901 locked.y -= rowsBelow;
1902 remaining.push_back(std::move(locked));
1904 lockedBlocks = std::move(remaining);
1907 [[nodiscard]]
bool isClearingRow(
int y)
const {
return lineClearActive && y >= 0 && y <
boardHeight && clearingRows[y]; }
1909 [[nodiscard]]
bool isLineClearVisible()
const {
1910 const auto now = std::chrono::steady_clock::now();
1911 const float elapsed = std::chrono::duration<float>(now - lineClearStart).count();
1912 constexpr float blinkIntervalSeconds = 0.12f;
1913 return (
static_cast<int>(elapsed / blinkIntervalSeconds) % 2) == 0;
1916 void drawIntroOverlay(VkCommandBuffer cmd,
const VkExtent2D &extent) {
1921 const auto now = std::chrono::steady_clock::now();
1922 const float elapsed = std::chrono::duration<float>(now - introStart).count();
1923 const float fadeProgress = std::clamp((elapsed - introHoldSeconds) / introFadeSeconds, 0.0f, 1.0f);
1924 const float alpha = 1.0f - fadeProgress;
1926 introSprite->setShaderParams(elapsed, 0.0f, 0.0f, alpha);
1927 introSprite->drawSpriteRect(0, 0,
static_cast<int>(extent.width),
static_cast<int>(extent.height));
1928 vkCmdBindPipeline(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS,
sprite_pipeline);
1930 introSprite->clearQueue();
1933 void drawGameOverOverlay(VkCommandBuffer cmd,
const VkExtent2D &extent) {
1939 const float alpha = gameOverFadeAlpha();
1940 gameOverSprite->setShaderParams(0.0f, 0.0f, 0.0f, alpha);
1941 gameOverSprite->drawSpriteRect(0, 0,
static_cast<int>(extent.width),
static_cast<int>(extent.height));
1942 vkCmdBindPipeline(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS,
sprite_pipeline);
1944 gameOverSprite->clearQueue();
1947 [[nodiscard]] glm::vec3 blockPosition(
int x,
int y)
const {
1948 const float worldX = (
static_cast<float>(x) - (
static_cast<float>(
boardWidth) - 1.0f) * 0.5f) * cubeSpacing;
1949 const float worldY = (
static_cast<float>(y) - (
static_cast<float>(
boardHeight) - 1.0f) * 0.5f) * cubeSpacing;
1950 return glm::vec3(worldX, worldY, 0.0f);
1953 [[nodiscard]] glm::mat4 blockMatrix(
int x,
int y,
float scale = cubeScale)
const {
1954 glm::mat4 model(1.0f);
1955 model = glm::translate(model, blockPosition(x, y));
1956 model = glm::scale(model, glm::vec3(scale));
1960 void drawBlock(VkCommandBuffer cmd, uint32_t imageIndex, BlockModel &block,
int x,
int y,
int color,
const glm::mat4 &view,
const glm::mat4 &proj) {
1961 mxvk::UniformBufferObject ubo{};
1962 ubo.
model = blockMatrix(x, y);
1965 ubo.
fx = glm::vec4(colorTints[color], 1.0f);
1966 block.model->updateUBO(imageIndex, ubo);
1967 block.model->render(cmd, imageIndex,
false);
1970 void drawFrame(VkCommandBuffer cmd, uint32_t imageIndex,
const glm::mat4 &view,
const glm::mat4 &proj) {
1973 drawFrameBlock(cmd, imageIndex, frameModels[index++], -1, y, view, proj);
1974 drawFrameBlock(cmd, imageIndex, frameModels[index++], boardWidth, y, view, proj);
1977 drawFrameBlock(cmd, imageIndex, frameModels[index++], x, -1, view, proj);
1981 void drawFrameBlock(VkCommandBuffer cmd, uint32_t imageIndex, BlockModel &block,
int x,
int y,
const glm::mat4 &view,
const glm::mat4 &proj) {
1982 mxvk::UniformBufferObject ubo{};
1983 ubo.
model = blockMatrix(x, y, cubeScale * 0.82f);
1986 ubo.
fx = glm::vec4(colorTints[7], 1.0f);
1987 block.model->updateUBO(imageIndex, ubo);
1988 block.model->render(cmd, imageIndex,
false);
1991 void drawSpriteRect(mxvk::VK_Sprite *sprite, VkCommandBuffer cmd,
const VkExtent2D &extent,
int x,
int y,
int w,
int h) {
1992 if (sprite ==
nullptr) {
1996 vkCmdBindPipeline(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS,
sprite_pipeline);
2001 void drawNextPiecePreview(VkCommandBuffer cmd, [[maybe_unused]] uint32_t imageIndex,
const VkExtent2D &extent) {
2006 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)});
2007 const int panelW = panelSize;
2008 const int panelH = panelSize;
2009 const int margin = 24;
2010 const int panelX =
static_cast<int>(extent.width) - panelW - margin;
2011 const int panelY = 88;
2012 const int border = 4;
2014 drawSpriteRect(previewBorderSprite, cmd, extent, panelX, panelY, panelW, border);
2015 drawSpriteRect(previewBorderSprite, cmd, extent, panelX, panelY + panelH - border, panelW, border);
2016 drawSpriteRect(previewBorderSprite, cmd, extent, panelX, panelY, border, panelH);
2017 drawSpriteRect(previewBorderSprite, cmd, extent, panelX + panelW - border, panelY, border, panelH);
2019 printText(
"Next", panelX + 12, panelY - 28, SDL_Color{255, 255, 255, 255});
2021 int minX = nextPiece.blocks[0].x;
2022 int maxX = nextPiece.blocks[0].x;
2023 int minY = nextPiece.blocks[0].y;
2024 int maxY = nextPiece.blocks[0].y;
2025 for (
const Block &block : nextPiece.blocks) {
2026 minX = std::min(minX, block.x);
2027 maxX = std::max(maxX, block.x);
2028 minY = std::min(minY, block.y);
2029 maxY = std::max(maxY, block.y);
2032 const float innerPadding = 28.0f;
2033 const float innerSize =
static_cast<float>(panelSize) - innerPadding * 2.0f;
2034 const int blockSize =
static_cast<int>(std::min(34.0f, innerSize /
static_cast<float>(std::max(maxX - minX + 1, maxY - minY + 1))));
2035 const float pieceW =
static_cast<float>(maxX - minX + 1) * blockSize;
2036 const float pieceH =
static_cast<float>(maxY - minY + 1) * blockSize;
2037 const float centerX =
static_cast<float>(panelX) +
static_cast<float>(panelW) * 0.5f;
2038 const float centerY =
static_cast<float>(panelY) +
static_cast<float>(panelH) * 0.5f;
2039 const float originX = centerX - pieceW * 0.5f;
2040 const float originY = centerY - pieceH * 0.5f;
2041 mxvk::VK_Sprite *blockSprite = blockPreviewSprites[
static_cast<std::size_t
>(nextPiece.color)];
2043 for (std::size_t i = 0; i < nextPiece.blocks.size(); ++i) {
2044 const Block &pieceBlock = nextPiece.blocks[i];
2045 const float x = originX +
static_cast<float>(pieceBlock.x - minX) * blockSize;
2046 const float y = originY +
static_cast<float>(pieceBlock.y - minY) * blockSize;
2047 drawSpriteRect(blockSprite, cmd, extent,
static_cast<int>(x),
static_cast<int>(y), blockSize, blockSize);
2051 void drawOpponentGrid(VkCommandBuffer cmd,
const VkExtent2D &extent) {
2056 const int margin = 24;
2057 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)});
2058 const int previewBottom = 88 + previewSize;
2059 const int panelTop = previewBottom + 56;
2060 const int textAreaHeight = 78;
2061 const int panelBottom =
static_cast<int>(extent.height) - margin - textAreaHeight;
2062 const int availableHeight = panelBottom - panelTop;
2063 const int availableWidth = std::min(260,
static_cast<int>(
static_cast<float>(extent.width) * 0.24f));
2064 if (availableWidth < 70 || availableHeight < 120) {
2067 const int blockSize = std::max(4, std::min(availableWidth / boardWidth, availableHeight / boardHeight));
2070 const int panelX =
static_cast<int>(extent.width) - gridW - margin;
2071 const int panelY = panelTop + std::max(0, (availableHeight - gridH) / 2);
2072 const int border = std::max(2, blockSize / 5);
2073 const auto textXInsideRightEdge = [
this, &extent, margin, panelX](
const std::initializer_list<std::string> &lines) {
2075 for (
const std::string &line : lines) {
2079 maxWidth = std::max(maxWidth, width);
2082 return std::max(margin, std::min(panelX,
static_cast<int>(extent.width) - margin - maxWidth));
2085 drawSpriteRect(previewBorderSprite, cmd, extent, panelX - border, panelY - border, gridW + border * 2, border);
2086 drawSpriteRect(previewBorderSprite, cmd, extent, panelX - border, panelY + gridH, gridW + border * 2, border);
2087 drawSpriteRect(previewBorderSprite, cmd, extent, panelX - border, panelY - border, border, gridH + border * 2);
2088 drawSpriteRect(previewBorderSprite, cmd, extent, panelX + gridW, panelY - border, border, gridH + border * 2);
2090 printText(
"Opponent", panelX, panelY - 30, SDL_Color{255, 255, 255, 255});
2091 if (!opponentSnapshot.hasState) {
2092 const std::string waitingText =
"Waiting...";
2093 printText(waitingText, textXInsideRightEdge({waitingText}), panelY + gridH + 12, SDL_Color{255, 220, 120, 255});
2099 const int color = opponentSnapshot.cells[y][x];
2100 if (color < 0 || color >=
static_cast<int>(blockPreviewSprites.size())) {
2103 const int screenX = panelX + x * blockSize;
2104 const int screenY = panelY + (
boardHeight - 1 - y) * blockSize;
2105 drawSpriteRect(blockPreviewSprites[
static_cast<std::size_t
>(color)], cmd, extent, screenX, screenY, blockSize, blockSize);
2109 if (!opponentSnapshot.gameOver) {
2110 for (
const Block &block : opponentSnapshot.activeBlocks) {
2111 const int x = opponentSnapshot.activeX + block.x;
2112 const int y = opponentSnapshot.activeY + block.y;
2113 if (x < 0 || x >= boardWidth || y < 0 || y >= boardHeight) {
2116 const int screenX = panelX + x * blockSize;
2117 const int screenY = panelY + (
boardHeight - 1 - y) * blockSize;
2118 drawSpriteRect(blockPreviewSprites[
static_cast<std::size_t
>(opponentSnapshot.activeColor)], cmd, extent, screenX, screenY, blockSize, blockSize);
2122 const std::string scoreText = std::format(
"Score {}", opponentSnapshot.score);
2123 const std::string linesText = std::format(
"Lines {} Lv {}", opponentSnapshot.lines, opponentSnapshot.level);
2124 const std::string gameOverText =
"Game Over";
2125 const int statsX = textXInsideRightEdge({scoreText, linesText, gameOverText});
2126 printText(scoreText, statsX, panelY + gridH + 12, SDL_Color{255, 255, 255, 255});
2127 printText(linesText, statsX, panelY + gridH + 38, SDL_Color{180, 220, 255, 255});
2128 if (opponentSnapshot.gameOver) {
2129 printText(gameOverText, statsX, panelY + gridH + 64, SDL_Color{255, 120, 120, 255});
2133 void drawCreditsModel(VkCommandBuffer cmd, uint32_t imageIndex,
const VkExtent2D &extent) {
2134 if (creditsTuxModel ==
nullptr) {
2138 const float aspect = (extent.height > 0U) ?
static_cast<float>(extent.width) /
static_cast<float>(extent.height) : 1.0f;
2139 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));
2140 view = glm::rotate(view, glm::radians(-10.0f), glm::vec3(1.0f, 0.0f, 0.0f));
2141 glm::mat4 proj = glm::perspective(glm::radians(40.0f), aspect, 0.1f, 100.0f);
2142 proj[1][1] *= -1.0f;
2144 const float elapsed =
static_cast<float>(SDL_GetTicks()) / 1000.0f;
2145 glm::mat4 model(1.0f);
2146 model = glm::translate(model, glm::vec3(0.0f, -0.34f, 0.0f));
2147 model = glm::rotate(model, elapsed * 0.75f, glm::vec3(0.0f, 1.0f, 0.0f));
2148 model = glm::rotate(model, std::sin(elapsed * 0.6f) * 0.08f, glm::vec3(1.0f, 0.0f, 0.0f));
2149 model = glm::scale(model, glm::vec3(0.228f));
2151 mxvk::UniformBufferObject ubo{};
2155 ubo.
fx = glm::vec4(1.0f, 1.0f, 1.0f, elapsed);
2156 creditsTuxModel->updateUBO(imageIndex, ubo);
2157 creditsTuxModel->render(cmd, imageIndex,
false);
2165 const int centerX =
static_cast<int>(extent.width) / 2;
2166 const float alpha = screenFadeAlpha();
2167 const auto withAlpha = [alpha](SDL_Color color) {
2168 color.a =
static_cast<Uint8
>(
static_cast<float>(color.a) * alpha);
2174 const int titleY =
static_cast<int>(
static_cast<float>(extent.height) * 0.18f);
2175 const int menuY =
static_cast<int>(
static_cast<float>(extent.height) * 0.40f);
2176 const int spacing =
static_cast<int>(std::max(34.0f,
static_cast<float>(extent.height) * 0.07f));
2177 printCenteredText(
"MXVK 3D Tetris", centerX, titleY, withAlpha(SDL_Color{255, 255, 0, 255}));
2178 printCenteredText(
"Choose a mode", centerX, titleY + 42, withAlpha(SDL_Color{255, 255, 255, 255}));
2180 const char *items[] = {
"New Game",
"Network Multiplayer",
"High Scores",
"Credits"};
2181 for (
int i = 0; i < 4; ++i) {
2182 const SDL_Color color = (i == cursorPos) ? SDL_Color{255, 255, 0, 255} : SDL_Color{255, 255, 255, 255};
2183 printCenteredText(items[i], centerX, menuY + i * spacing, withAlpha(color));
2185 printCenteredText(
"Use arrows and Enter", centerX,
static_cast<int>(
static_cast<float>(extent.height) * 0.86f), withAlpha(SDL_Color{180, 180, 180, 255}));
2189 const int baseY =
static_cast<int>(
static_cast<float>(extent.height) * 0.16f);
2190 printCenteredText(
"High Scores", centerX, baseY, withAlpha(SDL_Color{255, 255, 0, 255}));
2191 const auto &scores = highScores.entries();
2192 const int listLeftX =
static_cast<int>(
static_cast<float>(extent.width) * 0.34f);
2193 const int listRightX =
static_cast<int>(
static_cast<float>(extent.width) * 0.70f);
2194 const int lineHeight = 30;
2195 for (std::size_t i = 0; i < 10U; ++i) {
2196 const int rowY = baseY + 54 +
static_cast<int>(i) * lineHeight;
2197 const bool hasScore = i < scores.size();
2198 const std::string nameText = hasScore ? scores[i].name :
"---";
2199 const std::string scoreText = hasScore ? std::format(
"{}", scores[i].score) :
"---";
2200 printText(std::format(
"{:>2}. {}", i + 1U, nameText), listLeftX, rowY, withAlpha(SDL_Color{255, 255, 255, 255}));
2202 int scoreHeight = 0;
2204 printText(scoreText, listRightX - scoreWidth, rowY, withAlpha(SDL_Color{255, 255, 255, 255}));
2206 printText(scoreText, listRightX, rowY, withAlpha(SDL_Color{255, 255, 255, 255}));
2209 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}));
2213 const int baseY =
static_cast<int>(
static_cast<float>(extent.height) * 0.2f);
2214 printCenteredText(
"Credits", centerX, baseY, withAlpha(SDL_Color{255, 255, 0, 255}));
2215 printCenteredText(
"MXVK 3D Tetris", centerX, baseY + 48, withAlpha(SDL_Color{255, 255, 255, 255}));
2216 printCenteredText(
"Built with Vulkan and SDL3", centerX, baseY + 84, withAlpha(SDL_Color{255, 220, 120, 255}));
2217 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}));
2221 const int baseY =
static_cast<int>(
static_cast<float>(extent.height) * 0.16f);
2222 printCenteredText(
"Network Multiplayer", centerX, baseY, withAlpha(SDL_Color{255, 255, 0, 255}));
2223 printCenteredText(std::format(
"Port: {}", multiplayerPort), centerX, baseY + 42, withAlpha(SDL_Color{255, 220, 120, 255}));
2224 printCenteredText(std::format(
"Peer IP: {}", multiplayerHost.empty() ?
"_" : multiplayerHost), centerX, baseY + 86, withAlpha(SDL_Color{255, 255, 255, 255}));
2225 printCenteredText(multiplayerStatus, centerX, baseY + 126, withAlpha(SDL_Color{180, 220, 255, 255}));
2226 printCenteredText(
"H host J/Enter join Backspace edit", centerX, baseY + 178, withAlpha(SDL_Color{255, 255, 255, 255}));
2227 printCenteredText(
"Run one copy as host, then connect from the other machine by IP.", centerX, baseY + 216, withAlpha(SDL_Color{200, 200, 200, 255}));
2228 printCenteredText(
"Escape returns to menu", centerX,
static_cast<int>(
static_cast<float>(extent.height) * 0.82f), withAlpha(SDL_Color{200, 200, 200, 255}));
2232 const int baseY =
static_cast<int>(
static_cast<float>(extent.height) * 0.20f);
2233 const float gameOverAlpha = gameOverFadeAlpha();
2234 const auto withGameOverAlpha = [gameOverAlpha](SDL_Color color) {
2235 color.a =
static_cast<Uint8
>(
static_cast<float>(color.a) * gameOverAlpha);
2239 printCenteredText(
"Game Over", centerX, baseY, withGameOverAlpha(SDL_Color{255, 255, 0, 255}));
2240 printCenteredText(std::format(
"Final Score: {}", score), centerX, baseY + 42, withGameOverAlpha(SDL_Color{255, 255, 255, 255}));
2241 if (!multiplayerResult.empty()) {
2242 printCenteredText(multiplayerResult, centerX, baseY + 82, withGameOverAlpha(SDL_Color{255, 220, 120, 255}));
2243 printCenteredText(
"Enter to restart", centerX, baseY + 124, withGameOverAlpha(SDL_Color{200, 200, 200, 255}));
2244 printCenteredText(
"Escape for menu", centerX, baseY + 158, withGameOverAlpha(SDL_Color{200, 200, 200, 255}));
2248 printCenteredText(
"New high score", centerX, baseY + 82, withGameOverAlpha(SDL_Color{255, 220, 120, 255}));
2249 printCenteredText(std::format(
"Name: {}", playerName.empty() ?
"_" : playerName), centerX, baseY + 118, withGameOverAlpha(SDL_Color{255, 255, 255, 255}));
2250 printCenteredText(std::format(
"Pick: [{}]", currentNameCharacter()), centerX, baseY + 154, withGameOverAlpha(SDL_Color{120, 255, 255, 255}));
2251 printCenteredText(
"Up/Down choose, A/Space add", centerX, baseY + 190, withGameOverAlpha(SDL_Color{200, 200, 200, 255}));
2252 printCenteredText(
"X/Enter save, B/Backspace delete", centerX, baseY + 224, withGameOverAlpha(SDL_Color{200, 200, 200, 255}));
2254 printCenteredText(
"R to restart from intro", centerX, baseY + 92, withGameOverAlpha(SDL_Color{255, 220, 120, 255}));
2255 printCenteredText(
"H for high scores", centerX, baseY + 126, withGameOverAlpha(SDL_Color{200, 200, 200, 255}));
2256 printCenteredText(
"Enter to restart", centerX, baseY + 160, withGameOverAlpha(SDL_Color{200, 200, 200, 255}));
2261 printText(std::format(
"Score: {}", score), 15, 15, withAlpha(SDL_Color{255, 255, 255, 255}));
2262 printText(std::format(
"Lines Cleared: {}", linesCleared), 15, 45, withAlpha(SDL_Color{255, 220, 120, 255}));
2263 printText(std::format(
"Level: {}", level), 15, 75, withAlpha(SDL_Color{120, 220, 255, 255}));
2264 if (multiplayerActive) {
2265 printText(multiplayerHostSide ?
"Multiplayer: Host" :
"Multiplayer: Guest", 15, 105, withAlpha(SDL_Color{180, 255, 180, 255}));
2268 const int baseY =
static_cast<int>(
static_cast<float>(extent.height) * 0.58f);
2269 const float gameOverAlpha = gameOverFadeAlpha();
2270 const auto withGameOverAlpha = [gameOverAlpha](SDL_Color color) {
2271 color.a =
static_cast<Uint8
>(
static_cast<float>(color.a) * gameOverAlpha);
2274 printCenteredText(std::format(
"Final Score: {}", score), centerX, baseY, withGameOverAlpha(SDL_Color{255, 255, 255, 255}));
2275 printCenteredText(multiplayerResult.empty() ?
"Game over" : multiplayerResult, centerX, baseY + 40, withGameOverAlpha(SDL_Color{255, 220, 120, 255}));
2283 void printCenteredText(
const std::string &text,
int centerX,
int y,
const SDL_Color &color) {
2287 printText(text, centerX - textWidth / 2, y, color);
2293 void triggerBackgroundTransition() {
2294 backgroundTransitionActive =
true;
2295 backgroundTransitionStart = std::chrono::steady_clock::now();
2298 void updateBackgroundTransitionState() {
2299 if (!backgroundTransitionActive) {
2302 const auto now = std::chrono::steady_clock::now();
2303 const float elapsed = std::chrono::duration<float>(now - backgroundTransitionStart).count();
2304 if (elapsed >= backgroundTransitionSeconds) {
2305 backgroundTransitionActive =
false;
2309 [[nodiscard]] mxvk::VK_Sprite *screenSpriteFor(AppScreen screen)
const {
2312 return menuBackgroundSprite;
2314 return highScoresBackgroundSprite;
2316 return creditsBackgroundSprite;
2318 return multiplayerBackgroundSprite;
2327 void drawFadedSprite(mxvk::VK_Sprite *sprite, VkCommandBuffer cmd,
const VkExtent2D &extent,
float alpha) {
2332 sprite->
drawSpriteRect(0, 0,
static_cast<int>(extent.width),
static_cast<int>(extent.height));
2333 vkCmdBindPipeline(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS,
sprite_pipeline);
2338 void drawGameScreenTransitionOverlay(VkCommandBuffer cmd,
const VkExtent2D &extent) {
2342 drawFadedSprite(screenSpriteFor(transitionFromScreen), cmd, extent, 1.0f - screenFadeAlpha());
2345 void drawScreenBackdrop(VkCommandBuffer cmd,
const VkExtent2D &extent) {
2350 updateBackgroundTransitionState();
2352 mxvk::VK_Sprite *sprite = background;
2353 if (backgroundTransitionActive && backgroundTransitionSprite !=
nullptr) {
2354 sprite = backgroundTransitionSprite;
2356 if (sprite ==
nullptr) {
2360 if (sprite == backgroundTransitionSprite) {
2361 const auto now = std::chrono::steady_clock::now();
2362 const float elapsed = std::chrono::duration<float>(now - backgroundTransitionStart).count();
2366 sprite->
drawSpriteRect(0, 0,
static_cast<int>(extent.width),
static_cast<int>(extent.height));
2367 vkCmdBindPipeline(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS,
sprite_pipeline);
2373 const float alpha = screenFadeAlpha();
2374 if (screenTransitionActive) {
2375 drawFadedSprite(screenSpriteFor(transitionFromScreen), cmd, extent, 1.0f - alpha);
2377 drawFadedSprite(screenSpriteFor(screen), cmd, extent, alpha);