MXVK Vulkan Framework 0.35.0
C++20 Vulkan rendering framework for practical 2D and 3D application development with SDL3.
Loading...
Searching...
No Matches
puzzle_drop.cpp
Go to the documentation of this file.
1#include <SDL3/SDL.h>
2
3#include <algorithm>
4#include <array>
5#include <chrono>
6#include <cmath>
7#include <cstdlib>
8#include <format>
9#include <iostream>
10#include <memory>
11#include <random>
12#include <string>
13
14#include <glm/ext/matrix_clip_space.hpp>
15#include <glm/ext/matrix_transform.hpp>
16#include <glm/glm.hpp>
17
18#include "mxvk/argz.hpp"
19#include "mxvk/mxvk.hpp"
22#include "rain.hpp"
23
24#ifndef puzzle_drop_ASSET_DIR
25#define puzzle_drop_ASSET_DIR "."
26#endif
27
28namespace {
29 constexpr int BOARD_WIDTH = 20;
30 constexpr int BOARD_HEIGHT = 22;
31 constexpr float CUBE_SCALE = 0.048f;
32 constexpr float CUBE_SPACING = CUBE_SCALE * 1.12f;
33 constexpr std::array<float, 3> FALL_SECONDS{0.86f, 0.68f, 0.50f};
34 constexpr float INTRO_FADE_STEP = 0.01f;
35 constexpr Uint32 INTRO_FADE_INTERVAL_MS = 35U;
36 constexpr float INTRO_START_FADE = 1.0f;
37 constexpr int MATRIX_RAIN_TEXTURE_WIDTH = 1280;
38 constexpr int MATRIX_RAIN_TEXTURE_HEIGHT = 720;
39 constexpr size_t FRAME_TEXTURE_INDEX = 10;
40 constexpr size_t LEVEL_GRAPHIC_COUNT = 8;
41 constexpr size_t PREVIEW_BLOCK_TEXTURE_COUNT = 10;
42
43 enum class BlockType {
44 Null = 0,
45 Clear,
46 Red1,
47 Red2,
48 Red3,
49 Green1,
50 Green2,
51 Green3,
52 Blue1,
53 Blue2,
54 Blue3,
55 Match,
56 };
57
58 enum class ShiftDirection {
59 Down,
60 Up,
61 };
62
63 struct Block {
64 int x = 0;
65 int y = 0;
67 };
68
69 struct Piece {
70 std::array<Block, 3> blocks{};
71 int position = 0;
72
73 void new_piece(int start_x, int start_y, std::mt19937 &rng) {
74 blocks[0] = {start_x, start_y, random_type(rng)};
75 blocks[1] = {start_x, start_y + 1, random_type(rng)};
76 blocks[2] = {start_x, start_y + 2, random_type(rng)};
77 position = 0;
78 }
79
80 void shift(ShiftDirection direction) {
81 const std::array<BlockType, 3> types{blocks[0].type, blocks[1].type, blocks[2].type};
82 if (direction == ShiftDirection::Down) {
83 blocks[0].type = types[2];
84 blocks[1].type = types[0];
85 blocks[2].type = types[1];
86 } else {
87 blocks[0].type = types[1];
88 blocks[1].type = types[2];
89 blocks[2].type = types[0];
90 }
91 }
92
93 void move_left() {
94 for (Block &block : blocks) {
95 --block.x;
96 }
97 }
98
99 void move_right() {
100 for (Block &block : blocks) {
101 ++block.x;
102 }
103 }
104
105 void move_down() {
106 for (Block &block : blocks) {
107 ++block.y;
108 }
109 }
110
111 void rotate_left() {
112 if (position == 0) {
113 blocks[1].y -= 1;
114 blocks[1].x -= 1;
115 blocks[2].x -= 2;
116 blocks[2].y -= 2;
117 position = 1;
118 } else if (position == 1) {
119 blocks[1].y += 1;
120 blocks[1].x += 1;
121 blocks[2].y += 2;
122 blocks[2].x += 2;
123 position = 0;
124 }
125 }
126
128 if (position == 0) {
129 blocks[1].x += 1;
130 blocks[1].y -= 1;
131 blocks[2].x += 2;
132 blocks[2].y -= 2;
133 position = 2;
134 } else if (position == 2) {
135 blocks[1].x -= 1;
136 blocks[1].y += 1;
137 blocks[2].x -= 2;
138 blocks[2].y += 2;
139 position = 0;
140 }
141 }
142
143 private:
144 [[nodiscard]] static BlockType random_type(std::mt19937 &rng) {
145 std::uniform_int_distribution<int> dist(static_cast<int>(BlockType::Red1), static_cast<int>(BlockType::Match));
146 return static_cast<BlockType>(dist(rng));
147 }
148 };
149
155
156 const std::array<glm::vec3, 10> BLOCK_TINTS{{
157 {1.00f, 0.48f, 0.42f},
158 {1.00f, 0.58f, 0.50f},
159 {1.00f, 0.72f, 0.62f},
160 {0.42f, 0.95f, 0.55f},
161 {0.55f, 1.00f, 0.65f},
162 {0.70f, 1.00f, 0.78f},
163 {0.38f, 0.62f, 1.00f},
164 {0.50f, 0.74f, 1.00f},
165 {0.66f, 0.86f, 1.00f},
166 {1.00f, 0.92f, 0.35f},
167 }};
168
169 const std::array<std::string, PREVIEW_BLOCK_TEXTURE_COUNT> PREVIEW_BLOCK_TEXTURE_FILES{{
170 "red1.png",
171 "red2.png",
172 "red3.png",
173 "green1.png",
174 "green2.png",
175 "green3.png",
176 "blue1.png",
177 "blue2.png",
178 "blue3.png",
179 "red3.png",
180 }};
181
182 [[nodiscard]] bool is_play_block(BlockType type) { return type >= BlockType::Red1 && type <= BlockType::Match; }
183
184 [[nodiscard]] int texture_index(BlockType type) {
185 if (!is_play_block(type)) {
186 return 0;
187 }
188 return static_cast<int>(type) - static_cast<int>(BlockType::Red1);
189 }
190
191 [[nodiscard]] bool same_or_match(BlockType actual, BlockType expected) { return actual == expected || actual == BlockType::Match; }
192
193 class PuzzleDropWindow final : public mxvk::VK_Window {
194 public:
195 PuzzleDropWindow(const std::string &path, int width, int height, bool fullscreen, bool enable_vsync) : mxvk::VK_Window("-[ MXVK 3D PuzzleDrop ]-", width, height, fullscreen, MXVK_VALIDATION, enable_vsync), asset_root((path.empty() || path == ".") ? std::string(puzzle_drop_ASSET_DIR) : path), data_root(asset_root + "/data"), shader_root(data_root), tetris_data_root(data_root) {
196 std::random_device rd;
197 rng.seed(rd());
198 setClearColor(0.03f, 0.04f, 0.05f, 1.0f);
199 setFont(data_root + "/font.ttf", 22);
200 std::array<std::string, LEVEL_GRAPHIC_COUNT> level_graphics{};
201 for (size_t i = 0; i < level_graphics.size(); ++i) {
202 level_graphics[i] = std::format("{}/level{}.png", data_root, i + 1);
203 }
204 std::shuffle(level_graphics.begin(), level_graphics.end(), rng);
205 for (size_t i = 0; i < backgrounds.size(); ++i) {
206 backgrounds[i] = createSprite(level_graphics[i], shader_root + "/sprite.vert.spv", shader_root + "/puzzle_drop_background.frag.spv");
207 }
208 intro_sprite = createSprite(std::format("{}/intro1.png", data_root), shader_root + "/sprite.vert.spv", shader_root + "/intro.frag.spv");
209 matrix::RainConfig rain_config = matrix::make_matrix_rain_config(asset_root, false);
210 rain_config.color = "#2f8dff";
213 matrix_rain = std::make_unique<matrix::Rain>(*this, std::move(rain_config));
214 try_open_first_gamepad();
215 preview_border_sprite = createSprite(1, 1);
216 const uint32_t white_pixel = 0xFFFFFFFFu;
217 preview_border_sprite->updateTexture(&white_pixel, 1, 1);
218 for (std::size_t i = 0; i < preview_block_sprites.size(); ++i) {
219 preview_block_sprites[i] = createSprite(data_root + "/" + PREVIEW_BLOCK_TEXTURE_FILES[i]);
220 }
221 init_cube_model();
222 reset_game();
223 reset_intro_screen();
224 }
225
226 ~PuzzleDropWindow() override {
227 if (device != VK_NULL_HANDLE) {
228 vkDeviceWaitIdle(device);
229 }
230 close_gamepad();
231 cleanup_models();
232 }
233
234 void onSwapchainRecreated() override {
235 if (cube_model) {
236 cube_model->resize(this);
237 }
238 if (grid_backdrop_model) {
239 grid_backdrop_model->resize(this);
240 }
241 if (matrix_rain) {
242 matrix_rain->on_swapchain_recreated(*this);
243 }
244 }
245
246 void event(SDL_Event &e) override {
247 if (e.type == SDL_EVENT_QUIT) {
248 exit();
249 return;
250 }
251
252 if (e.type == SDL_EVENT_GAMEPAD_ADDED) {
253 if (!open_gamepad(e.gdevice.which)) {
254 try_open_first_gamepad();
255 }
256 return;
257 }
258
259 if (e.type == SDL_EVENT_GAMEPAD_REMOVED) {
260 if (gamepad != nullptr && e.gdevice.which == gamepad_id) {
261 close_gamepad();
262 try_open_first_gamepad();
263 }
264 return;
265 }
266
267 if (e.type == SDL_EVENT_GAMEPAD_BUTTON_DOWN) {
268 handle_gamepad_button_down(e.gbutton.button);
269 return;
270 }
271
272 if (e.type != SDL_EVENT_KEY_DOWN || e.key.repeat) {
273 return;
274 }
275
276 if (intro_active && (e.key.key == SDLK_SPACE || e.key.key == SDLK_RETURN || e.key.key == SDLK_KP_ENTER)) {
277 skip_intro();
278 return;
279 }
280
281 switch (e.key.key) {
282 case SDLK_ESCAPE:
283 exit();
284 break;
285 case SDLK_RETURN:
286 case SDLK_KP_ENTER:
287 if (game_over) {
288 reset_game();
289 game_started = true;
290 }
291 break;
292 case SDLK_1:
293 difficulty = 0;
294 reset_game();
295 game_started = true;
296 break;
297 case SDLK_2:
298 difficulty = 1;
299 reset_game();
300 game_started = true;
301 break;
302 case SDLK_3:
303 difficulty = 2;
304 reset_game();
305 game_started = true;
306 break;
307 case SDLK_Z:
308 rotate_left();
309 break;
310 case SDLK_X:
311 rotate_right();
312 break;
313 default:
314 break;
315 }
316 }
317
318 void proc() override {
319 const auto now = std::chrono::steady_clock::now();
320 const float delta_seconds = std::chrono::duration<float>(now - last_input_update).count();
321 last_input_update = now;
322 try_open_first_gamepad();
323 randomize_wildcard_color();
324
325 if (intro_active) {
326 update_intro(now);
327 } else {
328 background_time += delta_seconds;
329 const bool *keys = SDL_GetKeyboardState(nullptr);
330 if (keys != nullptr) {
331 handle_view_controls(keys, delta_seconds);
332 handle_piece_controls(keys, delta_seconds);
333 }
334 handle_gamepad_input(delta_seconds);
335 }
336
337 if (game_started && !game_over) {
338 if (std::chrono::duration<float>(now - last_fall).count() >= FALL_SECONDS[difficulty]) {
339 key_down();
340 last_fall = now;
341 }
342 if (std::chrono::duration<float>(now - last_process).count() >= 0.018f) {
343 proc_blocks();
344 proc_move_down();
345 last_process = now;
346 }
347 }
348
349 const SDL_Color primary{255, 244, 223, 255};
350 const SDL_Color secondary{218, 229, 232, 255};
351 if (intro_active) {
352 printText("Press Enter", 24, 54, secondary);
353 return;
354 }
355
356 if (game_over) {
357 print_game_over_text(secondary);
358 return;
359 }
360
361 printText(std::format("Level {} Lines {} Difficulty {}", level, lines, difficulty + 1), 24, 22, primary);
362 }
363
364 void onRecordCustomRendering(VkCommandBuffer cmd, uint32_t image_index) override {
365 const VkExtent2D extent = getSwapchainExtent();
366 if (intro_active) {
367 draw_game_scene(cmd, image_index, extent);
368 render_intro(cmd, extent);
369 return;
370 }
371
372 draw_game_scene(cmd, image_index, extent);
373 }
374
375 private:
376 std::string asset_root;
377 std::string data_root;
378 std::string shader_root;
379 std::string tetris_data_root;
380 std::mt19937 rng{};
381 std::array<std::array<Cell, BOARD_WIDTH>, BOARD_HEIGHT> board{};
382 Piece piece{};
383 Piece next_piece{};
384 std::unique_ptr<mxvk::VKAbstractModel> cube_model{};
385 std::unique_ptr<mxvk::VKAbstractModel> grid_backdrop_model{};
386 std::array<mxvk::VK_Sprite *, LEVEL_GRAPHIC_COUNT> backgrounds{};
387 mxvk::VK_Sprite *intro_sprite = nullptr;
388 mxvk::VK_Sprite *preview_border_sprite = nullptr;
389 std::array<mxvk::VK_Sprite *, PREVIEW_BLOCK_TEXTURE_COUNT> preview_block_sprites{};
390 std::unique_ptr<matrix::Rain> matrix_rain{};
391 SDL_Gamepad *gamepad = nullptr;
392 SDL_JoystickID gamepad_id = 0;
393 std::chrono::steady_clock::time_point last_fall{std::chrono::steady_clock::now()};
394 std::chrono::steady_clock::time_point last_process{std::chrono::steady_clock::now()};
395 std::chrono::steady_clock::time_point last_input_update{std::chrono::steady_clock::now()};
396 float horizontal_move_timer = 0.0f;
397 float soft_drop_timer = 0.0f;
398 float cycle_timer = 0.0f;
399 float gamepad_move_repeat_timer = 0.0f;
400 float gamepad_soft_drop_repeat_timer = 0.0f;
401 float gamepad_cycle_repeat_timer = 0.0f;
402 float gamepad_move_held_seconds = 0.0f;
403 int horizontal_move_direction = 0;
404 int gamepad_move_direction = 0;
405 bool soft_drop_held = false;
406 bool cycle_held = false;
407 bool gamepad_soft_drop_held = false;
408 bool gamepad_cycle_held = false;
409 int difficulty = 0;
410 int level = 1;
411 int lines = 0;
412 glm::vec3 wildcard_color{1.0f, 0.0f, 1.0f};
413 bool intro_active = true;
414 float intro_fade = INTRO_START_FADE;
415 std::chrono::steady_clock::time_point intro_last_update{std::chrono::steady_clock::now()};
416 std::chrono::steady_clock::time_point intro_start{std::chrono::steady_clock::now()};
417 float background_time = 0.0f;
418 bool game_started = false;
419 bool game_over = false;
420 float grid_yaw = -10.0f;
421 float grid_pitch = -8.0f;
422 float camera_distance = 2.32f;
423 static constexpr Sint16 GAMEPAD_DEADZONE = 10000;
424 static constexpr float GAMEPAD_MOVE_INITIAL_DELAY_SECONDS = 0.22f;
425 static constexpr float GAMEPAD_MOVE_REPEAT_SECONDS = 0.12f;
426 static constexpr float GAMEPAD_SOFT_DROP_INITIAL_DELAY_SECONDS = 0.18f;
427 static constexpr float GAMEPAD_SOFT_DROP_REPEAT_SECONDS = 0.08f;
428 static constexpr float GAMEPAD_CYCLE_INITIAL_DELAY_SECONDS = 0.16f;
429 static constexpr float GAMEPAD_CYCLE_REPEAT_SECONDS = 0.11f;
430 static constexpr float GAMEPAD_STICK_ROTATE_SPEED = 120.0f;
431 static constexpr float GAMEPAD_STICK_PITCH_SPEED = 100.0f;
432 static constexpr float GAMEPAD_STICK_SCALE = 1.0f / 32768.0f;
433
434 void draw_game_scene(VkCommandBuffer cmd, uint32_t image_index, const VkExtent2D &extent) {
435 draw_background(cmd, extent);
436
437 const float aspect = (extent.height > 0U) ? static_cast<float>(extent.width) / static_cast<float>(extent.height) : 1.0f;
438 glm::mat4 view = glm::lookAt(glm::vec3(0.0f, 0.12f, camera_distance), glm::vec3(0.0f, 0.05f, 0.0f), glm::vec3(0.0f, 1.0f, 0.0f));
439 view = glm::rotate(view, glm::radians(grid_pitch), glm::vec3(1.0f, 0.0f, 0.0f));
440 view = glm::rotate(view, glm::radians(grid_yaw), glm::vec3(0.0f, 1.0f, 0.0f));
441
442 glm::mat4 proj = glm::perspective(glm::radians(45.0f), aspect, 0.1f, 100.0f);
443 proj[1][1] *= -1.0f;
444
445 draw_grid_backdrop(cmd, image_index, view, proj);
446 draw_frame(cmd, image_index, view, proj);
447 for (int y = 0; y < BOARD_HEIGHT; ++y) {
448 for (int x = 0; x < BOARD_WIDTH; ++x) {
449 Cell &cell = board[y][x];
450 if (cell.type == BlockType::Null) {
451 continue;
452 }
453 if (cell.type == BlockType::Clear && ((cell.flash_counter / 6) % 2) != 0) {
454 continue;
455 }
456 draw_cube(cmd, image_index, cell.type, x, y, view, proj);
457 }
458 }
459
460 if ((game_started || intro_active) && !game_over) {
461 for (const Block &block : piece.blocks) {
462 draw_cube(cmd, image_index, block.type, block.x, block.y, view, proj);
463 }
464 }
465
466 draw_next_piece_preview(cmd, extent);
467 }
468
469 void randomize_wildcard_color() {
470 std::uniform_int_distribution<int> dist(0, 254);
471 wildcard_color = glm::vec3(static_cast<float>(dist(rng)) / 255.0f, static_cast<float>(dist(rng)) / 255.0f, static_cast<float>(dist(rng)) / 255.0f);
472 }
473
474 void handle_view_controls(const bool *keys, float delta_seconds) {
475 constexpr float YAW_SPEED = 115.0f;
476 constexpr float PITCH_SPEED = 90.0f;
477 constexpr float ZOOM_SPEED = 3.2f;
478 if (keys[SDL_SCANCODE_A]) {
479 grid_yaw -= YAW_SPEED * delta_seconds;
480 }
481 if (keys[SDL_SCANCODE_D]) {
482 grid_yaw += YAW_SPEED * delta_seconds;
483 }
484 if (keys[SDL_SCANCODE_W]) {
485 grid_pitch = std::clamp(grid_pitch + PITCH_SPEED * delta_seconds, -70.0f, 70.0f);
486 }
487 if (keys[SDL_SCANCODE_S]) {
488 grid_pitch = std::clamp(grid_pitch - PITCH_SPEED * delta_seconds, -70.0f, 70.0f);
489 }
490 if (keys[SDL_SCANCODE_PAGEUP]) {
491 camera_distance = std::max(1.35f, camera_distance - ZOOM_SPEED * delta_seconds);
492 }
493 if (keys[SDL_SCANCODE_PAGEDOWN]) {
494 camera_distance = std::min(7.0f, camera_distance + ZOOM_SPEED * delta_seconds);
495 }
496 }
497
498 void handle_piece_controls(const bool *keys, float delta_seconds) {
499 if (!game_started || game_over) {
500 reset_held_piece_input();
501 return;
502 }
503
504 const bool left = keys[SDL_SCANCODE_LEFT];
505 const bool right = keys[SDL_SCANCODE_RIGHT];
506 const int direction = (left == right) ? 0 : (left ? -1 : 1);
507 if (direction == 0) {
508 horizontal_move_direction = 0;
509 horizontal_move_timer = 0.0f;
510 } else {
511 constexpr float INITIAL_DELAY_SECONDS = 0.16f;
512 constexpr float REPEAT_SECONDS = 0.065f;
513 if (horizontal_move_direction != direction) {
514 horizontal_move_direction = direction;
515 horizontal_move_timer = -INITIAL_DELAY_SECONDS;
516 move_piece_horizontal(direction);
517 } else {
518 horizontal_move_timer += delta_seconds;
519 while (horizontal_move_timer >= 0.0f) {
520 horizontal_move_timer -= REPEAT_SECONDS;
521 move_piece_horizontal(direction);
522 }
523 }
524 }
525
526 if (keys[SDL_SCANCODE_DOWN]) {
527 constexpr float SOFT_DROP_REPEAT_SECONDS = 0.045f;
528 if (!soft_drop_held) {
529 soft_drop_held = true;
530 soft_drop_timer = 0.0f;
531 key_down();
532 last_fall = std::chrono::steady_clock::now();
533 } else {
534 soft_drop_timer += delta_seconds;
535 while (soft_drop_timer >= SOFT_DROP_REPEAT_SECONDS) {
536 soft_drop_timer -= SOFT_DROP_REPEAT_SECONDS;
537 key_down();
538 last_fall = std::chrono::steady_clock::now();
539 }
540 }
541 } else {
542 soft_drop_held = false;
543 soft_drop_timer = 0.0f;
544 }
545
546 if (keys[SDL_SCANCODE_UP]) {
547 constexpr float CYCLE_INITIAL_DELAY_SECONDS = 0.16f;
548 constexpr float CYCLE_REPEAT_SECONDS = 0.11f;
549 if (!cycle_held) {
550 cycle_held = true;
551 cycle_timer = -CYCLE_INITIAL_DELAY_SECONDS;
552 cycle_piece_blocks();
553 } else {
554 cycle_timer += delta_seconds;
555 while (cycle_timer >= 0.0f) {
556 cycle_timer -= CYCLE_REPEAT_SECONDS;
557 cycle_piece_blocks();
558 }
559 }
560 } else {
561 cycle_held = false;
562 cycle_timer = 0.0f;
563 }
564 }
565
566 void handle_gamepad_input(float delta_seconds) {
567 if (gamepad == nullptr || !game_started || game_over) {
568 reset_held_gamepad_input();
569 return;
570 }
571
572 const Sint16 left_x = SDL_GetGamepadAxis(gamepad, SDL_GAMEPAD_AXIS_LEFTX);
573 const Sint16 left_y = SDL_GetGamepadAxis(gamepad, SDL_GAMEPAD_AXIS_LEFTY);
574 const Sint16 right_x = SDL_GetGamepadAxis(gamepad, SDL_GAMEPAD_AXIS_RIGHTX);
575 const Sint16 right_y = SDL_GetGamepadAxis(gamepad, SDL_GAMEPAD_AXIS_RIGHTY);
576
577 const bool dpad_left = SDL_GetGamepadButton(gamepad, SDL_GAMEPAD_BUTTON_DPAD_LEFT);
578 const bool dpad_right = SDL_GetGamepadButton(gamepad, SDL_GAMEPAD_BUTTON_DPAD_RIGHT);
579 const bool dpad_down = SDL_GetGamepadButton(gamepad, SDL_GAMEPAD_BUTTON_DPAD_DOWN);
580 const bool dpad_up = SDL_GetGamepadButton(gamepad, SDL_GAMEPAD_BUTTON_DPAD_UP);
581
582 const int move_direction = dpad_left == dpad_right ? ((left_x < -GAMEPAD_DEADZONE) ? -1 : (left_x > GAMEPAD_DEADZONE) ? 1 : 0) : (dpad_left ? -1 : 1);
583 if (move_direction == 0) {
584 gamepad_move_direction = 0;
585 gamepad_move_held_seconds = 0.0f;
586 gamepad_move_repeat_timer = 0.0f;
587 } else if (move_direction != gamepad_move_direction) {
588 gamepad_move_direction = move_direction;
589 gamepad_move_held_seconds = 0.0f;
590 gamepad_move_repeat_timer = 0.0f;
591 move_piece_horizontal(gamepad_move_direction);
592 } else {
593 gamepad_move_held_seconds += delta_seconds;
594 const float threshold = (gamepad_move_held_seconds < GAMEPAD_MOVE_INITIAL_DELAY_SECONDS) ? GAMEPAD_MOVE_INITIAL_DELAY_SECONDS : GAMEPAD_MOVE_REPEAT_SECONDS;
595 gamepad_move_repeat_timer += delta_seconds;
596 if (gamepad_move_repeat_timer >= threshold) {
597 move_piece_horizontal(gamepad_move_direction);
598 gamepad_move_repeat_timer = 0.0f;
599 }
600 }
601
602 const bool soft_drop_down = dpad_down || left_y > GAMEPAD_DEADZONE;
603 if (!soft_drop_down) {
604 gamepad_soft_drop_held = false;
605 gamepad_soft_drop_repeat_timer = 0.0f;
606 } else {
607 const float threshold = gamepad_soft_drop_held ? GAMEPAD_SOFT_DROP_REPEAT_SECONDS : GAMEPAD_SOFT_DROP_INITIAL_DELAY_SECONDS;
608 gamepad_soft_drop_repeat_timer += delta_seconds;
609 if (gamepad_soft_drop_repeat_timer >= threshold) {
610 key_down();
611 last_fall = std::chrono::steady_clock::now();
612 gamepad_soft_drop_repeat_timer = 0.0f;
613 gamepad_soft_drop_held = true;
614 }
615 }
616
617 if (!dpad_up) {
618 gamepad_cycle_held = false;
619 gamepad_cycle_repeat_timer = 0.0f;
620 } else {
621 const float threshold = gamepad_cycle_held ? GAMEPAD_CYCLE_REPEAT_SECONDS : GAMEPAD_CYCLE_INITIAL_DELAY_SECONDS;
622 gamepad_cycle_repeat_timer += delta_seconds;
623 if (gamepad_cycle_repeat_timer >= threshold) {
624 cycle_piece_blocks();
625 gamepad_cycle_repeat_timer = 0.0f;
626 gamepad_cycle_held = true;
627 }
628 }
629
630 if (std::abs(right_x) > GAMEPAD_DEADZONE) {
631 grid_yaw += static_cast<float>(right_x) * GAMEPAD_STICK_SCALE * GAMEPAD_STICK_ROTATE_SPEED * delta_seconds;
632 }
633 if (std::abs(right_y) > GAMEPAD_DEADZONE) {
634 grid_pitch = std::clamp(grid_pitch - static_cast<float>(right_y) * GAMEPAD_STICK_SCALE * GAMEPAD_STICK_PITCH_SPEED * delta_seconds, -70.0f, 70.0f);
635 }
636
637 constexpr float ZOOM_SPEED = 3.2f;
638 if (SDL_GetGamepadButton(gamepad, SDL_GAMEPAD_BUTTON_LEFT_SHOULDER)) {
639 camera_distance = std::min(7.0f, camera_distance + ZOOM_SPEED * delta_seconds);
640 }
641 if (SDL_GetGamepadButton(gamepad, SDL_GAMEPAD_BUTTON_RIGHT_SHOULDER)) {
642 camera_distance = std::max(1.35f, camera_distance - ZOOM_SPEED * delta_seconds);
643 }
644 }
645
646 void handle_gamepad_button_down(Uint8 button) {
647 if (intro_active) {
648 if (button == SDL_GAMEPAD_BUTTON_SOUTH || button == SDL_GAMEPAD_BUTTON_START) {
649 skip_intro();
650 }
651 return;
652 }
653
654 if (game_over) {
655 if (button == SDL_GAMEPAD_BUTTON_SOUTH || button == SDL_GAMEPAD_BUTTON_START) {
656 reset_game();
657 game_started = true;
658 } else if (button == SDL_GAMEPAD_BUTTON_BACK) {
659 exit();
660 }
661 return;
662 }
663
664 if (!game_started) {
665 return;
666 }
667
668 if (button == SDL_GAMEPAD_BUTTON_SOUTH) {
669 rotate_right();
670 } else if (button == SDL_GAMEPAD_BUTTON_WEST) {
671 rotate_left();
672 } else if (button == SDL_GAMEPAD_BUTTON_EAST) {
673 hard_drop();
674 } else if (button == SDL_GAMEPAD_BUTTON_BACK) {
675 exit();
676 }
677 }
678
679 void reset_held_piece_input() {
680 horizontal_move_timer = 0.0f;
681 soft_drop_timer = 0.0f;
682 cycle_timer = 0.0f;
683 horizontal_move_direction = 0;
684 soft_drop_held = false;
685 cycle_held = false;
686 }
687
688 void reset_held_gamepad_input() {
689 gamepad_move_repeat_timer = 0.0f;
690 gamepad_soft_drop_repeat_timer = 0.0f;
691 gamepad_cycle_repeat_timer = 0.0f;
692 gamepad_move_held_seconds = 0.0f;
693 gamepad_move_direction = 0;
694 gamepad_soft_drop_held = false;
695 gamepad_cycle_held = false;
696 }
697
698 void move_piece_horizontal(int direction) {
699 if (!check_piece(piece, direction, 0)) {
700 return;
701 }
702 if (direction < 0) {
703 piece.move_left();
704 } else {
705 piece.move_right();
706 }
707 }
708
709 void cycle_piece_blocks() { piece.shift(ShiftDirection::Up); }
710
711 void hard_drop() {
712 if (!game_started || game_over) {
713 return;
714 }
715 while (check_piece(piece, 0, 1)) {
716 piece.move_down();
717 }
718 key_down();
719 last_fall = std::chrono::steady_clock::now();
720 }
721
722 bool open_gamepad(SDL_JoystickID id) {
723 if (gamepad != nullptr && gamepad_id == id) {
724 return true;
725 }
726 close_gamepad();
727 gamepad = SDL_OpenGamepad(id);
728 if (gamepad == nullptr) {
729 return false;
730 }
731 gamepad_id = id;
732 return true;
733 }
734
735 void close_gamepad() {
736 if (gamepad != nullptr) {
737 SDL_CloseGamepad(gamepad);
738 gamepad = nullptr;
739 gamepad_id = 0;
740 }
741 }
742
743 void try_open_first_gamepad() {
744 if (gamepad != nullptr) {
745 return;
746 }
747 int count = 0;
748 SDL_JoystickID *ids = SDL_GetGamepads(&count);
749 if (ids == nullptr || count <= 0) {
750 if (ids != nullptr) {
751 SDL_free(ids);
752 }
753 return;
754 }
755 open_gamepad(ids[0]);
756 SDL_free(ids);
757 }
758
759 void reset_game() {
760 reset_held_piece_input();
761 reset_held_gamepad_input();
762 for (auto &row : board) {
763 for (Cell &cell : row) {
764 cell.type = BlockType::Null;
765 cell.clear_value = 0;
766 cell.flash_counter = 0;
767 }
768 }
769 level = 1;
770 lines = 0;
771 game_over = false;
772 piece.new_piece(BOARD_WIDTH / 2, 0, rng);
773 next_piece.new_piece(BOARD_WIDTH / 2, 0, rng);
774 last_fall = std::chrono::steady_clock::now();
775 last_process = last_fall;
776 }
777
778 void key_down() {
779 if (check_piece(piece, 0, 1)) {
780 piece.move_down();
781 return;
782 }
783 set_piece();
784 piece = next_piece;
785 next_piece.new_piece(BOARD_WIDTH / 2, 0, rng);
786 if (!check_piece(piece, 0, 0)) {
787 game_over = true;
788 }
789 }
790
791 [[nodiscard]] bool check_piece(const Piece &test_piece, int offset_x, int offset_y) const {
792 for (const Block &block : test_piece.blocks) {
793 const int x = block.x + offset_x;
794 const int y = block.y + offset_y;
795 if (x < 0 || x >= BOARD_WIDTH || y < 0 || y >= BOARD_HEIGHT) {
796 return false;
797 }
798 const BlockType type = board[y][x].type;
799 if (type != BlockType::Null && type != BlockType::Clear) {
800 return false;
801 }
802 }
803 return true;
804 }
805
806 void set_piece() {
807 for (const Block &block : piece.blocks) {
808 if (block.x < 0 || block.x >= BOARD_WIDTH || block.y < 0 || block.y >= BOARD_HEIGHT) {
809 continue;
810 }
811 Cell &cell = board[block.y][block.x];
812 cell.type = block.type;
813 cell.clear_value = 0;
814 cell.flash_counter = 0;
815 if (block.y == 0) {
816 game_over = true;
817 }
818 }
819 }
820
821 void rotate_left() {
822 if (!game_started || game_over) {
823 return;
824 }
825 Piece test_piece = piece;
826 test_piece.rotate_left();
827 if (check_piece(test_piece, 0, 0)) {
828 piece.rotate_left();
829 }
830 }
831
832 void rotate_right() {
833 if (!game_started || game_over) {
834 return;
835 }
836 Piece test_piece = piece;
837 test_piece.rotate_right();
838 if (check_piece(test_piece, 0, 0)) {
839 piece.rotate_right();
840 }
841 }
842
843 bool proc_blocks() {
844 constexpr std::array<std::array<int, 2>, 4> directions{{
845 {{1, 0}},
846 {{0, 1}},
847 {{1, 1}},
848 {{1, -1}},
849 }};
850 constexpr std::array<BlockType, 3> color_starts{BlockType::Red1, BlockType::Green1, BlockType::Blue1};
851
852 for (int y = 0; y < BOARD_HEIGHT; ++y) {
853 for (int x = 0; x < BOARD_WIDTH; ++x) {
854 for (const auto &direction : directions) {
855 for (BlockType start : color_starts) {
856 const BlockType one = start;
857 const BlockType two = static_cast<BlockType>(static_cast<int>(start) + 1);
858 const BlockType three = static_cast<BlockType>(static_cast<int>(start) + 2);
859 if (check_sequence(x, y, direction[0], direction[1], one, two, three) || check_sequence(x, y, direction[0], direction[1], three, two, one)) {
860 mark_clear(x, y, direction[0], direction[1]);
861 add_score();
862 return true;
863 }
864 }
865 }
866 }
867 }
868 return false;
869 }
870
871 bool proc_move_down() {
872 for (int y = BOARD_HEIGHT - 2; y >= 0; --y) {
873 for (int x = 0; x < BOARD_WIDTH; ++x) {
874 Cell &source = board[y][x];
875 Cell &target = board[y + 1][x];
876 if (is_play_block(source.type) && target.type == BlockType::Null) {
877 target.type = source.type;
878 target.clear_value = source.clear_value;
879 target.flash_counter = source.flash_counter;
880 source.type = BlockType::Null;
881 source.clear_value = 0;
882 source.flash_counter = 0;
883 return true;
884 }
885 }
886 }
887
888 bool updated = false;
889 for (auto &row : board) {
890 for (Cell &cell : row) {
891 if (cell.type == BlockType::Clear) {
892 ++cell.clear_value;
893 ++cell.flash_counter;
894 if (cell.clear_value > 50) {
895 cell.type = BlockType::Null;
896 cell.clear_value = 0;
897 cell.flash_counter = 0;
898 }
899 updated = true;
900 }
901 }
902 }
903 return updated;
904 }
905
906 [[nodiscard]] bool check_sequence(int x, int y, int dx, int dy, BlockType first, BlockType second, BlockType third) const { return check_block(x, y, first) && check_block(x + dx, y + dy, second) && check_block(x + dx * 2, y + dy * 2, third); }
907
908 [[nodiscard]] bool check_block(int x, int y, BlockType expected) const {
909 if (x < 0 || x >= BOARD_WIDTH || y < 0 || y >= BOARD_HEIGHT) {
910 return false;
911 }
912 return same_or_match(board[y][x].type, expected);
913 }
914
915 void mark_clear(int x, int y, int dx, int dy) {
916 for (int i = 0; i < 3; ++i) {
917 Cell &cell = board[y + dy * i][x + dx * i];
918 cell.type = BlockType::Clear;
919 cell.clear_value = 1;
920 cell.flash_counter = 0;
921 }
922 }
923
924 void add_score() {
925 ++lines;
926 if ((lines % 6) == 0 && level < static_cast<int>(backgrounds.size())) {
927 ++level;
928 }
929 }
930
931 void init_cube_model() {
932 cube_model = std::make_unique<mxvk::VKAbstractModel>();
933 cube_model->load(this, tetris_data_root + "/cube.mxmod.z", data_root + "/cube_textures.txt", data_root, 1.0f);
934 cube_model->setShaders(this, shader_root + "/puzzle_drop_piece.vert.spv", shader_root + "/puzzle_drop_piece.frag.spv");
935
936 grid_backdrop_model = std::make_unique<mxvk::VKAbstractModel>();
937 grid_backdrop_model->load(this, tetris_data_root + "/cube.mxmod.z", tetris_data_root + "/manifest_gray.txt", tetris_data_root, 1.0f);
938 grid_backdrop_model->setShaders(this, shader_root + "/puzzle_drop_piece.vert.spv", shader_root + "/puzzle_drop_piece.frag.spv");
939 grid_backdrop_model->setAlphaBlending(true);
940 }
941
942 void cleanup_models() {
943 if (cube_model) {
944 cube_model->cleanup(this);
945 cube_model.reset();
946 }
947 if (grid_backdrop_model) {
948 grid_backdrop_model->cleanup(this);
949 grid_backdrop_model.reset();
950 }
951 }
952
953 void print_game_over_text(const SDL_Color &color) {
954 const std::string text = std::format("Game Over: Lines cleared: {} [Press Enter to Restart]", lines);
955 const VkExtent2D extent = getSwapchainExtent();
956 int text_width = 0;
957 int text_height = 0;
958 if (!getTextDimensions(text, text_width, text_height)) {
959 printText(text, 24, 54, color);
960 return;
961 }
962
963 const int screen_width = static_cast<int>(extent.width);
964 const int screen_height = static_cast<int>(extent.height);
965 const int x = std::max(0, (screen_width - text_width) / 2);
966 const int y = std::max(0, (screen_height - text_height) / 2);
967 printText(text, x, y, color);
968 }
969
970 void reset_intro_screen() {
971 intro_active = true;
972 intro_fade = INTRO_START_FADE;
973 intro_last_update = std::chrono::steady_clock::now();
974 intro_start = intro_last_update;
975 game_started = false;
976 if (matrix_rain) {
977 matrix_rain->set_opacity(1.0f);
978 matrix_rain->reset();
979 }
980 reset_held_piece_input();
981 reset_held_gamepad_input();
982 }
983
984 void skip_intro() {
985 intro_fade = std::min(intro_fade, 0.02f);
986 intro_last_update = std::chrono::steady_clock::now() - std::chrono::milliseconds(INTRO_FADE_INTERVAL_MS);
987 }
988
989 void finish_intro(const std::chrono::steady_clock::time_point &now) {
990 intro_active = false;
991 intro_fade = 0.0f;
992 if (matrix_rain) {
993 matrix_rain->set_opacity(0.0f);
994 }
995 game_started = true;
996 last_fall = now;
997 last_process = now;
998 last_input_update = now;
999 reset_held_piece_input();
1000 reset_held_gamepad_input();
1001 }
1002
1003 void update_intro(const std::chrono::steady_clock::time_point &now) {
1004 const auto elapsed_ms = std::chrono::duration_cast<std::chrono::milliseconds>(now - intro_last_update).count();
1005 if (elapsed_ms > static_cast<long long>(INTRO_FADE_INTERVAL_MS)) {
1006 intro_last_update = now;
1007 intro_fade -= INTRO_FADE_STEP;
1008 }
1009 if (intro_fade <= 0.0f) {
1010 finish_intro(now);
1011 }
1012 }
1013
1014 void render_intro(VkCommandBuffer cmd, const VkExtent2D &extent) {
1015 if (intro_sprite == nullptr || sprite_pipeline == VK_NULL_HANDLE || sprite_pipeline_layout == VK_NULL_HANDLE) {
1016 finish_intro(std::chrono::steady_clock::now());
1017 return;
1018 }
1019
1020 const float elapsed = std::chrono::duration<float>(std::chrono::steady_clock::now() - intro_start).count();
1021 intro_sprite->setShaderParams(elapsed, 0.0f, 0.0f, std::clamp(intro_fade, 0.0f, 1.0f));
1022 intro_sprite->drawSpriteRect(0, 0, static_cast<int>(extent.width), static_cast<int>(extent.height));
1023 vkCmdBindPipeline(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, sprite_pipeline);
1024 intro_sprite->renderSprites(cmd, sprite_pipeline_layout, extent.width, extent.height);
1025 intro_sprite->clearQueue();
1026 render_matrix_rain(cmd, extent, std::clamp(intro_fade, 0.0f, 1.0f));
1027 }
1028
1029 void draw_background(VkCommandBuffer cmd, const VkExtent2D &extent) {
1030 const int index = std::clamp(level - 1, 0, static_cast<int>(backgrounds.size()) - 1);
1031 mxvk::VK_Sprite *background = backgrounds[index];
1032 if (background == nullptr || sprite_pipeline == VK_NULL_HANDLE || sprite_pipeline_layout == VK_NULL_HANDLE) {
1033 return;
1034 }
1035 background->setShaderParams(background_time, 0.0f, 0.0f, 1.0f);
1036 background->drawSpriteRect(0, 0, static_cast<int>(extent.width), static_cast<int>(extent.height));
1037 vkCmdBindPipeline(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, sprite_pipeline);
1038 background->renderSprites(cmd, sprite_pipeline_layout, extent.width, extent.height);
1039 background->clearQueue();
1040 }
1041
1042 void draw_sprite_rect(mxvk::VK_Sprite *sprite, VkCommandBuffer cmd, const VkExtent2D &extent, int x, int y, int w, int h) {
1043 if (sprite == nullptr || w <= 0 || h <= 0 || sprite_pipeline == VK_NULL_HANDLE || sprite_pipeline_layout == VK_NULL_HANDLE) {
1044 return;
1045 }
1046 sprite->drawSpriteRect(x, y, w, h);
1047 vkCmdBindPipeline(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, sprite_pipeline);
1048 sprite->renderSprites(cmd, sprite_pipeline_layout, extent.width, extent.height);
1049 sprite->clearQueue();
1050 }
1051
1052 void draw_next_piece_preview(VkCommandBuffer cmd, const VkExtent2D &extent) {
1053 if (!game_started || intro_active || game_over || preview_border_sprite == nullptr || sprite_pipeline == VK_NULL_HANDLE || sprite_pipeline_layout == VK_NULL_HANDLE) {
1054 return;
1055 }
1056
1057 const int panel_size = std::min({180, static_cast<int>(static_cast<float>(extent.width) * 0.22f), static_cast<int>(static_cast<float>(extent.height) * 0.30f)});
1058 if (panel_size < 72) {
1059 return;
1060 }
1061
1062 const int margin = 24;
1063 const int panel_x = static_cast<int>(extent.width) - panel_size - margin;
1064 const int panel_y = 88;
1065 const int border = 4;
1066
1067 draw_sprite_rect(preview_border_sprite, cmd, extent, panel_x, panel_y, panel_size, border);
1068 draw_sprite_rect(preview_border_sprite, cmd, extent, panel_x, panel_y + panel_size - border, panel_size, border);
1069 draw_sprite_rect(preview_border_sprite, cmd, extent, panel_x, panel_y, border, panel_size);
1070 draw_sprite_rect(preview_border_sprite, cmd, extent, panel_x + panel_size - border, panel_y, border, panel_size);
1071
1072 printText("Next", panel_x + 12, panel_y - 28, SDL_Color{255, 255, 255, 255});
1073
1074 int min_x = next_piece.blocks[0].x;
1075 int max_x = next_piece.blocks[0].x;
1076 int min_y = next_piece.blocks[0].y;
1077 int max_y = next_piece.blocks[0].y;
1078 for (const Block &block : next_piece.blocks) {
1079 min_x = std::min(min_x, block.x);
1080 max_x = std::max(max_x, block.x);
1081 min_y = std::min(min_y, block.y);
1082 max_y = std::max(max_y, block.y);
1083 }
1084
1085 const float inner_padding = 28.0f;
1086 const float inner_size = static_cast<float>(panel_size) - inner_padding * 2.0f;
1087 const int cells_wide = max_x - min_x + 1;
1088 const int cells_high = max_y - min_y + 1;
1089 const int block_size = static_cast<int>(std::min(34.0f, inner_size / static_cast<float>(std::max(cells_wide, cells_high))));
1090 const float piece_w = static_cast<float>(cells_wide * block_size);
1091 const float piece_h = static_cast<float>(cells_high * block_size);
1092 const float center_x = static_cast<float>(panel_x) + static_cast<float>(panel_size) * 0.5f;
1093 const float center_y = static_cast<float>(panel_y) + static_cast<float>(panel_size) * 0.5f;
1094 const float origin_x = center_x - piece_w * 0.5f;
1095 const float origin_y = center_y - piece_h * 0.5f;
1096
1097 for (const Block &block : next_piece.blocks) {
1098 const int index = texture_index(block.type);
1099 if (index < 0 || index >= static_cast<int>(preview_block_sprites.size())) {
1100 continue;
1101 }
1102 mxvk::VK_Sprite *block_sprite = preview_block_sprites[static_cast<std::size_t>(index)];
1103 const float x = origin_x + static_cast<float>(block.x - min_x) * static_cast<float>(block_size);
1104 const float y = origin_y + static_cast<float>(block.y - min_y) * static_cast<float>(block_size);
1105 draw_sprite_rect(block_sprite, cmd, extent, static_cast<int>(x), static_cast<int>(y), block_size, block_size);
1106 }
1107 }
1108
1109 void render_matrix_rain(VkCommandBuffer cmd, const VkExtent2D &extent, float opacity) {
1110 if (matrix_rain == nullptr || sprite_pipeline == VK_NULL_HANDLE || sprite_pipeline_layout == VK_NULL_HANDLE) {
1111 return;
1112 }
1113 matrix_rain->set_opacity(opacity);
1114 matrix_rain->update_and_render(*this, static_cast<int>(extent.width), static_cast<int>(extent.height));
1115 mxvk::VK_Sprite *rain_sprite = matrix_rain->sprite();
1116 if (rain_sprite == nullptr) {
1117 return;
1118 }
1119 vkCmdBindPipeline(cmd, VK_PIPELINE_BIND_POINT_GRAPHICS, sprite_pipeline);
1120 rain_sprite->renderSprites(cmd, sprite_pipeline_layout, extent.width, extent.height);
1121 rain_sprite->clearQueue();
1122 }
1123
1124 [[nodiscard]] glm::vec3 block_position(int x, int y) const {
1125 const float world_x = (static_cast<float>(x) - (static_cast<float>(BOARD_WIDTH) - 1.0f) * 0.5f) * CUBE_SPACING;
1126 const float world_y = ((static_cast<float>(BOARD_HEIGHT) - 1.0f) * 0.5f - static_cast<float>(y)) * CUBE_SPACING;
1127 return glm::vec3(world_x, world_y, 0.0f);
1128 }
1129
1130 void draw_grid_backdrop(VkCommandBuffer cmd, uint32_t image_index, const glm::mat4 &view, const glm::mat4 &proj) {
1131 if (!grid_backdrop_model) {
1132 return;
1133 }
1134
1135 mxvk::UniformBufferObject ubo{};
1136 glm::mat4 model(1.0f);
1137 const float width = static_cast<float>(BOARD_WIDTH) * CUBE_SPACING;
1138 const float height = static_cast<float>(BOARD_HEIGHT) * CUBE_SPACING;
1139 model = glm::translate(model, glm::vec3(0.0f, 0.0f, -CUBE_SCALE * 0.68f));
1140 model = glm::scale(model, glm::vec3(width + CUBE_SPACING * 0.35f, height + CUBE_SPACING * 0.35f, CUBE_SCALE * 0.10f));
1141 ubo.model = model;
1142 ubo.view = view;
1143 ubo.proj = proj;
1144 ubo.fx = glm::vec4(0.16f, 0.16f, 0.17f, 0.62f);
1145 grid_backdrop_model->renderWithPushConstants(cmd, image_index, 0, ubo, false);
1146 }
1147
1148 [[nodiscard]] glm::mat4 block_matrix(int x, int y, float scale = CUBE_SCALE) const {
1149 glm::mat4 model(1.0f);
1150 model = glm::translate(model, block_position(x, y));
1151 model = glm::scale(model, glm::vec3(scale));
1152 return model;
1153 }
1154
1155 void draw_cube(VkCommandBuffer cmd, uint32_t image_index, BlockType type, int x, int y, const glm::mat4 &view, const glm::mat4 &proj) const {
1156 if (!cube_model) {
1157 return;
1158 }
1159
1160 mxvk::UniformBufferObject ubo{};
1161 ubo.model = block_matrix(x, y);
1162 ubo.view = view;
1163 ubo.proj = proj;
1164 const bool use_wildcard_texture = type == BlockType::Match || type == BlockType::Clear;
1165 const int index = texture_index(use_wildcard_texture ? BlockType::Match : type);
1166 glm::vec3 tint = BLOCK_TINTS[index];
1167 if (use_wildcard_texture) {
1168 tint = wildcard_color;
1169 }
1170 ubo.fx = glm::vec4(tint, use_wildcard_texture ? 2.0f : 1.0f);
1171 cube_model->renderWithPushConstants(cmd, image_index, static_cast<size_t>(index), ubo, false);
1172 }
1173
1174 void draw_frame(VkCommandBuffer cmd, uint32_t image_index, const glm::mat4 &view, const glm::mat4 &proj) const {
1175 for (int y = 0; y < BOARD_HEIGHT; ++y) {
1176 draw_frame_cube(cmd, image_index, -1, y, view, proj);
1177 draw_frame_cube(cmd, image_index, BOARD_WIDTH, y, view, proj);
1178 }
1179 for (int x = -1; x <= BOARD_WIDTH; ++x) {
1180 draw_frame_cube(cmd, image_index, x, BOARD_HEIGHT, view, proj);
1181 }
1182 }
1183
1184 void draw_frame_cube(VkCommandBuffer cmd, uint32_t image_index, int x, int y, const glm::mat4 &view, const glm::mat4 &proj) const {
1185 if (!cube_model) {
1186 return;
1187 }
1188
1189 mxvk::UniformBufferObject ubo{};
1190 ubo.model = block_matrix(x, y, CUBE_SCALE * 0.82f);
1191 ubo.view = view;
1192 ubo.proj = proj;
1193 ubo.fx = glm::vec4(0.58f, 0.62f, 0.66f, 1.0f);
1194 cube_model->renderWithPushConstants(cmd, image_index, FRAME_TEXTURE_INDEX, ubo, false);
1195 }
1196 };
1197} // namespace
1198
1199int main(int argc, char **argv) {
1200 try {
1201 Arguments args = proc_args(argc, argv);
1202 PuzzleDropWindow window(args.path, args.width, args.height, args.fullscreen, args.enable_vsync);
1203 window.loop();
1204 } catch (const mxvk::Exception &e) {
1205 std::cerr << std::format("mxvk: Exception: {}\n", e.text());
1206 return EXIT_FAILURE;
1207 } catch (const ArgException<std::string> &e) {
1208 std::cerr << std::format("mxvk: Argument Exception: {}\n", e.text());
1209 return EXIT_FAILURE;
1210 } catch (const std::exception &e) {
1211 std::cerr << std::format("puzzle_drop: Exception: {}\n", e.what());
1212 return EXIT_FAILURE;
1213 }
1214 return EXIT_SUCCESS;
1215}
Lightweight, header-only, template command-line argument parser.
Arguments proc_args(int &argc, char **argv)
Parse standard libmx2 command-line options from main()'s argv.
Definition argz.hpp:854
Exception thrown by Argz::proc() on unrecognised or malformed options.
Definition argz.hpp:169
PuzzleDropWindow(const std::string &path, int width, int height, bool fullscreen, bool enable_vsync)
void onSwapchainRecreated() override
Called after swapchain and render resources are recreated.
void proc() override
Execute one processing/update step.
void event(SDL_Event &e) override
Handle one SDL event.
void onRecordCustomRendering(VkCommandBuffer cmd, uint32_t image_index) override
Optional hook for derived classes to record extra draw commands.
std::string text() const
void renderSprites(VkCommandBuffer cmdBuffer, VkPipelineLayout pipelineLayout, uint32_t screenWidth, uint32_t screenHeight)
Record all queued draw commands into the given command buffer.
void setShaderParams(float p1=0.0f, float p2=0.0f, float p3=0.0f, float p4=0.0f)
Set up to four custom shader float parameters.
void clearQueue()
Discard all pending draw commands without rendering.
void drawSpriteRect(int x, int y, int w, int h)
Queue a draw into an explicit destination rectangle.
Main Vulkan window wrapper for MXVK.
Definition mxvk.hpp:39
VkPipelineLayout sprite_pipeline_layout
Definition mxvk.hpp:680
VkExtent2D getSwapchainExtent() const noexcept
Get the current swapchain extent.
Definition mxvk.hpp:222
VkDevice device
Definition mxvk.hpp:606
VK_Sprite * createSprite(const std::string &pngPath, const std::string &vertexShaderPath="", const std::string &fragmentShaderPath="")
Create a sprite from a PNG file and register it with this window.
Definition mxvk.cpp:4455
bool getTextDimensions(const std::string &text, int &width, int &height)
Measure text dimensions in pixels.
Definition mxvk.cpp:4004
void setClearColor(float r, float g, float b, float a=1.0f)
Set the per-frame color attachment clear color.
Definition mxvk.cpp:637
VkPipeline sprite_pipeline
Definition mxvk.hpp:681
void exit()
Request loop termination.
Definition mxvk.cpp:1378
VK_Window()=default
Construct an empty window object.
void setFont(const std::string &fontPath, int fontSize=24)
Set the active text-render font.
Definition mxvk.cpp:3872
void printText(const std::string &text, int x, int y, const SDL_Color &col)
Queue a text string for rendering during the current frame.
Definition mxvk.cpp:3919
#define MXVK_VALIDATION
Definition mxvk.hpp:28
High-level model wrapper integrated with MXVK dynamic rendering.
int texture_index(BlockType type)
Definition main.cpp:178
constexpr int BOARD_WIDTH
Definition main.cpp:32
bool same_or_match(BlockType actual, BlockType expected)
Definition main.cpp:180
constexpr int BOARD_HEIGHT
Definition main.cpp:33
bool is_play_block(BlockType type)
Definition main.cpp:176
const std::array< glm::vec3, 10 > BLOCK_TINTS
const std::array< std::string, PREVIEW_BLOCK_TEXTURE_COUNT > PREVIEW_BLOCK_TEXTURE_FILES
constexpr std::array< float, 3 > FALL_SECONDS
bool same_or_match(BlockType actual, BlockType expected)
int main()
Definition main.py:165
RainConfig make_matrix_rain_config(const std::string &asset_root, bool binary_glyph_mode)
Definition rain.cpp:142
Utilities for loading and saving PNG images.
Definition mxvk.hpp:31
#define puzzle_drop_ASSET_DIR
Plain data structure returned by proc_args() with all common libmx2 CLI options.
Definition argz.hpp:718
bool fullscreen
Whether fullscreen mode was requested.
Definition argz.hpp:724
bool enable_vsync
Enable FIFO present mode / v-sync (--enable-vsync).
Definition argz.hpp:738
int height
Viewport height in pixels (default: 720).
Definition argz.hpp:721
std::string path
Asset root; proc_args() defaults it to the executable directory.
Definition argz.hpp:723
int width
Viewport width in pixels (default: 1280).
Definition argz.hpp:720
void new_piece(int start_x, int start_y, std::mt19937 &rng)
void shift(ShiftDirection direction)
std::string color
Definition rain.hpp:19