From e0c2cd210a0bc6690f87dad03e0e3bed0214a04f Mon Sep 17 00:00:00 2001 From: enne2 Date: Thu, 21 May 2026 15:06:27 +0200 Subject: [PATCH] Initial commit of Miyoo Mini Plus port for CyberMatris --- .gitignore | 13 + Font.hpp | 108 +++++++ Game.cpp | 450 ++++++++++++++++++++++++++ Game.hpp | 189 +++++++++++ Makefile | 28 ++ Makefile.miyoo | 109 +++++++ Renderer.cpp | 845 ++++++++++++++++++++++++++++++++++++++++++++++++ Renderer.hpp | 74 +++++ Synth.cpp | 802 +++++++++++++++++++++++++++++++++++++++++++++ Synth.hpp | 125 +++++++ launch_miyoo.sh | 50 +++ main.cpp | 332 +++++++++++++++++++ screenshot.png | Bin 0 -> 9477 bytes 13 files changed, 3125 insertions(+) create mode 100644 .gitignore create mode 100644 Font.hpp create mode 100644 Game.cpp create mode 100644 Game.hpp create mode 100644 Makefile create mode 100644 Makefile.miyoo create mode 100644 Renderer.cpp create mode 100644 Renderer.hpp create mode 100644 Synth.cpp create mode 100644 Synth.hpp create mode 100644 launch_miyoo.sh create mode 100644 main.cpp create mode 100644 screenshot.png diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..a571893 --- /dev/null +++ b/.gitignore @@ -0,0 +1,13 @@ +# Build artifacts +*.o +cybermatris +cybermatris_miyoo +dist_miyoo/ + +# System / IDE files +.DS_Store +.vscode/ +.idea/ + +# Large screenshot BMP (keep png instead) +screenshot.bmp diff --git a/Font.hpp b/Font.hpp new file mode 100644 index 0000000..ba073ca --- /dev/null +++ b/Font.hpp @@ -0,0 +1,108 @@ +#pragma once +#include + +// A complete 8x8 bitmap font layout for printable ASCII characters from 32 (space) to 126 (~). +// Total: 95 characters. Each character is represented by 8 bytes. +// Each byte represents one row of 8 pixels from left to right (MSB to LSB). +namespace Font { + constexpr int WIDTH = 8; + constexpr int HEIGHT = 8; + + inline const uint8_t BITMAP[95][8] = { + {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // 32 (space) + {0x18, 0x3c, 0x3c, 0x18, 0x18, 0x00, 0x18, 0x00}, // 33 ! + {0x36, 0x36, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00}, // 34 " + {0x36, 0x36, 0x7f, 0x36, 0x7f, 0x36, 0x36, 0x00}, // 35 # + {0x1c, 0x3e, 0x61, 0x3c, 0x07, 0x83, 0x7c, 0x38}, // 36 $ + {0x63, 0x66, 0x0c, 0x18, 0x30, 0x66, 0xc6, 0x00}, // 37 % + {0x38, 0x6c, 0x38, 0x76, 0xdc, 0xcc, 0x76, 0x00}, // 38 & + {0x18, 0x18, 0x30, 0x00, 0x00, 0x00, 0x00, 0x00}, // 39 ' + {0x0c, 0x18, 0x30, 0x30, 0x30, 0x18, 0x0c, 0x00}, // 40 ( + {0x30, 0x18, 0x0c, 0x0c, 0x0c, 0x18, 0x30, 0x00}, // 41 ) + {0x00, 0x66, 0x3c, 0xff, 0x3c, 0x66, 0x00, 0x00}, // 42 * + {0x00, 0x18, 0x18, 0x7e, 0x18, 0x18, 0x00, 0x00}, // 43 + + {0x00, 0x00, 0x00, 0x00, 0x00, 0x18, 0x18, 0x30}, // 44 , + {0x00, 0x00, 0x00, 0x7e, 0x00, 0x00, 0x00, 0x00}, // 45 - + {0x00, 0x00, 0x00, 0x00, 0x00, 0x18, 0x18, 0x00}, // 46 . + {0x03, 0x06, 0x0c, 0x18, 0x30, 0x60, 0xc0, 0x00}, // 47 / + {0x3e, 0x63, 0x63, 0x6b, 0x63, 0x63, 0x3e, 0x00}, // 48 0 + {0x18, 0x38, 0x18, 0x18, 0x18, 0x18, 0x3c, 0x00}, // 49 1 + {0x3e, 0x63, 0x07, 0x0e, 0x1c, 0x38, 0x7f, 0x00}, // 50 2 + {0x3e, 0x63, 0x07, 0x1e, 0x07, 0x63, 0x3e, 0x00}, // 51 3 + {0x06, 0x0e, 0x1e, 0x36, 0x7f, 0x06, 0x06, 0x00}, // 52 4 + {0x7f, 0x60, 0x7e, 0x03, 0x03, 0x63, 0x3e, 0x00}, // 53 5 + {0x1e, 0x30, 0x60, 0x7e, 0x63, 0x63, 0x3e, 0x00}, // 54 6 + {0x7f, 0x63, 0x03, 0x06, 0x0c, 0x18, 0x18, 0x00}, // 55 7 + {0x3e, 0x63, 0x63, 0x3e, 0x63, 0x63, 0x3e, 0x00}, // 56 8 + {0x3e, 0x63, 0x63, 0x7f, 0x03, 0x06, 0x3c, 0x00}, // 57 9 + {0x00, 0x18, 0x18, 0x00, 0x18, 0x18, 0x00, 0x00}, // 58 : + {0x00, 0x18, 0x18, 0x00, 0x18, 0x18, 0x30, 0x00}, // 59 ; + {0x06, 0x0c, 0x18, 0x30, 0x18, 0x0c, 0x06, 0x00}, // 60 < + {0x00, 0x00, 0x7e, 0x00, 0x7e, 0x00, 0x00, 0x00}, // 61 = + {0x60, 0x30, 0x18, 0x0c, 0x18, 0x30, 0x60, 0x00}, // 62 > + {0x3e, 0x63, 0x07, 0x0e, 0x18, 0x00, 0x18, 0x00}, // 63 ? + {0x3e, 0x63, 0x6b, 0x6b, 0x6b, 0x3e, 0x00, 0x00}, // 64 @ + {0x18, 0x3c, 0x66, 0x66, 0x7e, 0x66, 0x66, 0x00}, // 65 A + {0x7c, 0x66, 0x66, 0x7c, 0x66, 0x66, 0x7c, 0x00}, // 66 B + {0x3e, 0x63, 0x60, 0x60, 0x60, 0x63, 0x3e, 0x00}, // 67 C + {0x78, 0x6c, 0x66, 0x66, 0x66, 0x6c, 0x78, 0x00}, // 68 D + {0x7f, 0x60, 0x60, 0x7c, 0x60, 0x60, 0x7f, 0x00}, // 69 E + {0x7f, 0x60, 0x60, 0x7c, 0x60, 0x60, 0x60, 0x00}, // 70 F + {0x3e, 0x63, 0x60, 0x6f, 0x63, 0x63, 0x3e, 0x00}, // 71 G + {0x66, 0x66, 0x66, 0x7e, 0x66, 0x66, 0x66, 0x00}, // 72 H + {0x3e, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x3e, 0x00}, // 73 I + {0x1f, 0x06, 0x06, 0x06, 0x06, 0x66, 0x3c, 0x00}, // 74 J + {0x66, 0x6c, 0x78, 0x70, 0x78, 0x6c, 0x66, 0x00}, // 75 K + {0x60, 0x60, 0x60, 0x60, 0x60, 0x60, 0x7f, 0x00}, // 76 L + {0x63, 0x77, 0x7f, 0x6b, 0x63, 0x63, 0x63, 0x00}, // 77 M + {0x63, 0x67, 0x6f, 0x7b, 0x73, 0x63, 0x63, 0x00}, // 78 N + {0x3e, 0x63, 0x63, 0x63, 0x63, 0x63, 0x3e, 0x00}, // 79 O + {0x7c, 0x66, 0x66, 0x7c, 0x60, 0x60, 0x60, 0x00}, // 80 P + {0x3e, 0x63, 0x63, 0x63, 0x6b, 0x6c, 0x3e, 0x03}, // 81 Q + {0x7c, 0x66, 0x66, 0x7c, 0x78, 0x6c, 0x66, 0x00}, // 82 R + {0x3e, 0x63, 0x60, 0x3e, 0x03, 0x63, 0x3e, 0x00}, // 83 S + {0x7f, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x00}, // 84 T + {0x63, 0x63, 0x63, 0x63, 0x63, 0x63, 0x3e, 0x00}, // 85 U + {0x63, 0x63, 0x63, 0x63, 0x63, 0x36, 0x1c, 0x00}, // 86 V + {0x63, 0x63, 0x63, 0x6b, 0x7f, 0x77, 0x63, 0x00}, // 87 W + {0x63, 0x66, 0x3c, 0x18, 0x3c, 0x66, 0x63, 0x00}, // 88 X + {0x63, 0x63, 0x63, 0x3e, 0x0c, 0x0c, 0x0c, 0x00}, // 89 Y + {0x7f, 0x06, 0x0c, 0x18, 0x30, 0x60, 0x7f, 0x00}, // 90 Z + {0x3c, 0x30, 0x30, 0x30, 0x30, 0x30, 0x3c, 0x00}, // 91 [ + {0xc0, 0x60, 0x30, 0x18, 0x0c, 0x06, 0x03, 0x00}, // 92 \ + {0x3c, 0x0c, 0x0c, 0x0c, 0x0c, 0x0c, 0x3c, 0x00}, // 93 ] + {0x10, 0x38, 0x6c, 0xc6, 0x00, 0x00, 0x00, 0x00}, // 94 ^ + {0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xff}, // 95 _ + {0x30, 0x30, 0x18, 0x00, 0x00, 0x00, 0x00, 0x00}, // 96 ` + {0x00, 0x00, 0x3c, 0x06, 0x3e, 0x66, 0x3b, 0x00}, // 97 a + {0x60, 0x60, 0x7c, 0x66, 0x66, 0x66, 0x7c, 0x00}, // 98 b + {0x00, 0x00, 0x3c, 0x66, 0x60, 0x66, 0x3c, 0x00}, // 99 c + {0x06, 0x06, 0x3e, 0x66, 0x66, 0x66, 0x3e, 0x00}, // 100 d + {0x00, 0x00, 0x3c, 0x66, 0x7e, 0x60, 0x3c, 0x00}, // 101 e + {0x1c, 0x36, 0x30, 0x7c, 0x30, 0x30, 0x30, 0x00}, // 102 f + {0x00, 0x00, 0x3e, 0x66, 0x66, 0x3e, 0x06, 0x3c}, // 103 g + {0x60, 0x60, 0x7c, 0x66, 0x66, 0x66, 0x66, 0x00}, // 104 h + {0x18, 0x00, 0x38, 0x18, 0x18, 0x18, 0x3c, 0x00}, // 105 i + {0x06, 0x00, 0x1e, 0x06, 0x06, 0x06, 0x66, 0x3c}, // 106 j + {0x60, 0x60, 0x66, 0x6c, 0x78, 0x6c, 0x66, 0x00}, // 107 k + {0x38, 0x18, 0x18, 0x18, 0x18, 0x18, 0x3c, 0x00}, // 108 l + {0x00, 0x00, 0x66, 0x7f, 0x5b, 0x49, 0x49, 0x00}, // 109 m + {0x00, 0x00, 0x7c, 0x66, 0x66, 0x66, 0x66, 0x00}, // 110 n + {0x00, 0x00, 0x3c, 0x66, 0x66, 0x66, 0x3c, 0x00}, // 111 o + {0x00, 0x00, 0x7c, 0x66, 0x66, 0x7c, 0x60, 0x60}, // 112 p + {0x00, 0x00, 0x3e, 0x66, 0x66, 0x3e, 0x06, 0x06}, // 113 q + {0x00, 0x00, 0x7c, 0x66, 0x60, 0x60, 0x60, 0x00}, // 114 r + {0x00, 0x00, 0x3e, 0x60, 0x3c, 0x06, 0x7c, 0x00}, // 115 s + {0x30, 0x30, 0x7c, 0x30, 0x30, 0x36, 0x1c, 0x00}, // 116 t + {0x00, 0x00, 0x66, 0x66, 0x66, 0x66, 0x3e, 0x00}, // 117 u + {0x00, 0x00, 0x66, 0x66, 0x66, 0x3c, 0x18, 0x00}, // 118 v + {0x00, 0x00, 0x63, 0x6b, 0x7f, 0x3e, 0x1c, 0x00}, // 119 w + {0x00, 0x00, 0x66, 0x3c, 0x18, 0x3c, 0x66, 0x00}, // 120 x + {0x00, 0x00, 0x66, 0x66, 0x66, 0x3e, 0x06, 0x3c}, // 121 y + {0x00, 0x00, 0x7e, 0x0c, 0x18, 0x30, 0x7e, 0x00}, // 122 z + {0x0c, 0x18, 0x18, 0x70, 0x18, 0x18, 0x0c, 0x00}, // 123 { + {0x18, 0x18, 0x18, 0x00, 0x18, 0x18, 0x18, 0x00}, // 124 | + {0x30, 0x18, 0x18, 0x0e, 0x18, 0x18, 0x30, 0x00}, // 125 } + {0x3b, 0x6e, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00} // 126 ~ + }; +} diff --git a/Game.cpp b/Game.cpp new file mode 100644 index 0000000..99b2cd5 --- /dev/null +++ b/Game.cpp @@ -0,0 +1,450 @@ +#include "Game.hpp" +#include +#include +#include + + + +// SRS Wall-Kick translation vectors (x, y) for 3x3 shapes (T, J, L, S, Z) +// Direction is either Clockwise (CW) or Counter-Clockwise (CCW). +// Format: KICKS_3x3[CW/CCW][StartRot][TestIndex] +static const Point KICKS_3x3_CW[4][5] = { + { {0, 0}, {-1, 0}, {-1, 1}, {0, -2}, {-1, -2} }, // 0 -> 1 + { {0, 0}, {1, 0}, {1, -1}, {0, 2}, {1, 2} }, // 1 -> 2 + { {0, 0}, {1, 0}, {1, 1}, {0, -2}, {1, -2} }, // 2 -> 3 + { {0, 0}, {-1, 0}, {-1, -1}, {0, 2}, {-1, 2} } // 3 -> 0 +}; + +static const Point KICKS_3x3_CCW[4][5] = { + { {0, 0}, {1, 0}, {1, 1}, {0, -2}, {1, -2} }, // 0 -> 3 + { {0, 0}, {1, 0}, {1, -1}, {0, 2}, {1, 2} }, // 1 -> 0 + { {0, 0}, {-1, 0}, {-1, 1}, {0, -2}, {-1, -2} }, // 2 -> 1 + { {0, 0}, {-1, 0}, {-1, -1}, {0, 2}, {-1, 2} } // 3 -> 2 +}; + +// SRS Wall-Kick translation vectors (x, y) for I piece (4x4) +static const Point KICKS_I_CW[4][5] = { + { {0, 0}, {-2, 0}, {1, 0}, {-2, 1}, {1, -2} }, // 0 -> 1 + { {0, 0}, {-1, 0}, {2, 0}, {-1, -2}, {2, 1} }, // 1 -> 2 + { {0, 0}, {2, 0}, {-1, 0}, {2, -1}, {-1, 2} }, // 2 -> 3 + { {0, 0}, {1, 0}, {-2, 0}, {1, 2}, {-2, -1} } // 3 -> 0 +}; + +static const Point KICKS_I_CCW[4][5] = { + { {0, 0}, {-1, 0}, {2, 0}, {-1, -2}, {2, 1} }, // 0 -> 3 + { {0, 0}, {2, 0}, {-1, 0}, {2, -1}, {-1, 2} }, // 1 -> 0 + { {0, 0}, {1, 0}, {-2, 0}, {1, 2}, {-2, -1} }, // 2 -> 1 + { {0, 0}, {-2, 0}, {1, 0}, {-2, 1}, {1, -2} } // 3 -> 2 +}; + +Game::Game() { + unsigned seed = std::chrono::system_clock::now().time_since_epoch().count(); + mRng.seed(seed); + init(); +} + +void Game::init() { + mState = GameState::START; + reset(); +} + +void Game::reset() { + // Clear grid + for (int y = 0; y < BOARD_HEIGHT; ++y) { + for (int x = 0; x < BOARD_WIDTH; ++x) { + mBoard[y][x] = PieceType::NONE; + } + } + + mScore = 0; + mLinesCleared = 0; + mLevel = 1; + mCombo = -1; + + mHoldType = PieceType::NONE; + mCanHold = true; + + mFallTimer = 0.0f; + mLockTimer = 0.0f; + mIsLocking = false; + mShakeIntensity = 0.0f; + + mBag.clear(); + mNextQueue.clear(); + + // Populate the queue with initial pieces + fillBag(); + for (int i = 0; i < 4; ++i) { + mNextQueue.push_back(popNextPiece()); + } + + mActiveType = PieceType::NONE; + spawnPiece(); +} + +void Game::fillBag() { + std::vector pieces = { + PieceType::I, PieceType::O, PieceType::T, PieceType::S, PieceType::Z, PieceType::J, PieceType::L + }; + std::shuffle(pieces.begin(), pieces.end(), mRng); + mBag.insert(mBag.end(), pieces.begin(), pieces.end()); +} + +PieceType Game::popNextPiece() { + if (mBag.empty()) { + fillBag(); + } + PieceType next = mBag.back(); + mBag.pop_back(); + return next; +} + +void Game::spawnPiece() { + mActiveType = mNextQueue.front(); + mNextQueue.erase(mNextQueue.begin()); + mNextQueue.push_back(popNextPiece()); + + mActiveRot = 0; + + // Spawn at top center + mActiveX = BOARD_WIDTH / 2 - 1; + mActiveY = 0; + + mCanHold = true; + mIsLocking = false; + mLockTimer = 0.0f; + + // Check game over right at spawn + if (fits(mActiveType, mActiveRot, mActiveX, mActiveY)) { + std::cout << "[DEBUG] Game Over triggered on spawn of piece type " << static_cast(mActiveType) << std::endl; + mState = GameState::GAME_OVER; + flagGameOverSFX = true; + } else { + std::cout << "[DEBUG] Spawned piece type " << static_cast(mActiveType) << " successfully at (" << mActiveX << ", " << mActiveY << ")" << std::endl; + } +} + +std::vector Game::getActiveCells() const { + std::vector cells; + if (mActiveType == PieceType::NONE) return cells; + + int typeIdx = static_cast(mActiveType); + for (int i = 0; i < 4; ++i) { + Point p = TETROMINO_CELLS[typeIdx][mActiveRot][i]; + cells.push_back({mActiveX + p.x, mActiveY + p.y}); + } + return cells; +} + +std::vector Game::getGhostCells() const { + std::vector cells = getActiveCells(); + if (cells.empty()) return cells; + + int dy = 0; + while (!checkCollision(cells, 0, dy + 1)) { + dy++; + } + + for (auto& p : cells) { + p.y += dy; + } + return cells; +} + +bool Game::checkCollision(const std::vector& cells, int dx, int dy) const { + for (const auto& p : cells) { + int nx = p.x + dx; + int ny = p.y + dy; + + // Bounds check + if (nx < 0 || nx >= BOARD_WIDTH || ny >= BOARD_HEIGHT) { + return true; + } + + // Top cap check (allow pieces to scroll off top grid temporarily) + if (ny < 0) continue; + + if (mBoard[ny][nx] != PieceType::NONE) { + return true; + } + } + return false; +} + +bool Game::fits(PieceType type, int rot, int cx, int cy) const { + int typeIdx = static_cast(type); + for (int i = 0; i < 4; ++i) { + Point p = TETROMINO_CELLS[typeIdx][rot][i]; + int nx = cx + p.x; + int ny = cy + p.y; + + if (nx < 0 || nx >= BOARD_WIDTH || ny >= BOARD_HEIGHT) { + return true; + } + if (ny < 0) continue; + + if (mBoard[ny][nx] != PieceType::NONE) { + return true; + } + } + return false; +} + +bool Game::moveLeft() { + if (mState != GameState::PLAYING) return false; + std::vector cells = getActiveCells(); + if (!checkCollision(cells, -1, 0)) { + mActiveX--; + flagMoveSFX = true; + // Slide kick resets lock timer + if (mIsLocking) { + mLockTimer = 0.0f; + } + return true; + } + return false; +} + +bool Game::moveRight() { + if (mState != GameState::PLAYING) return false; + std::vector cells = getActiveCells(); + if (!checkCollision(cells, 1, 0)) { + mActiveX++; + flagMoveSFX = true; + // Slide kick resets lock timer + if (mIsLocking) { + mLockTimer = 0.0f; + } + return true; + } + return false; +} + +bool Game::rotate(int dir) { + if (mState != GameState::PLAYING || mActiveType == PieceType::NONE) return false; + if (mActiveType == PieceType::O) return false; // O piece does not rotate + + int nextRot = (mActiveRot + dir + 4) % 4; + + // Get wall kick offset trials + const Point(*kicks)[5] = nullptr; + if (mActiveType == PieceType::I) { + kicks = (dir == 1) ? KICKS_I_CW : KICKS_I_CCW; + } else { + kicks = (dir == 1) ? KICKS_3x3_CW : KICKS_3x3_CCW; + } + + // Try all 5 Wall-kick tests + for (int t = 0; t < 5; ++t) { + int dx = kicks[mActiveRot][t].x; + int dy = kicks[mActiveRot][t].y; + + if (!fits(mActiveType, nextRot, mActiveX + dx, mActiveY + dy)) { + // Apply rotation and kick offset + mActiveRot = nextRot; + mActiveX += dx; + mActiveY += dy; + flagRotateSFX = true; + + // Reset lock delay on successful rotation + if (mIsLocking) { + mLockTimer = 0.0f; + } + return true; + } + } + return false; +} + +void Game::softDrop() { + if (mState != GameState::PLAYING) return; + std::vector cells = getActiveCells(); + if (!checkCollision(cells, 0, 1)) { + mActiveY++; + mScore += 1; // Soft drop score + mFallTimer = 0.0f; + } +} + +bool Game::hardDrop() { + if (mState != GameState::PLAYING) return false; + std::vector cells = getActiveCells(); + int dy = 0; + while (!checkCollision(cells, 0, dy + 1)) { + dy++; + } + + mActiveY += dy; + mScore += dy * 2; // Hard drop score + + // Force instant lock + mShakeIntensity = 0.15f + (dy * 0.015f); // hard drop screenshake! + lockPiece(); + return true; +} + +void Game::holdPiece() { + if (mState != GameState::PLAYING || !mCanHold) return; + + flagRotateSFX = true; // cool swoosh + PieceType temp = mHoldType; + mHoldType = mActiveType; + + if (temp == PieceType::NONE) { + spawnPiece(); + } else { + mActiveType = temp; + mActiveRot = 0; + mActiveX = BOARD_WIDTH / 2 - 1; + mActiveY = 0; + mIsLocking = false; + mLockTimer = 0.0f; + } + + mCanHold = false; +} + +float Game::getFallDelay() const { + // Standard progressive fall timing (seconds per grid step) + switch (mLevel) { + case 1: return 0.85f; + case 2: return 0.72f; + case 3: return 0.60f; + case 4: return 0.48f; + case 5: return 0.38f; + case 6: return 0.30f; + case 7: return 0.22f; + case 8: return 0.16f; + case 9: return 0.11f; + case 10: return 0.08f; + default: return 0.06f; // level 11+ + } +} + +bool Game::update(float dt) { + if (mState != GameState::PLAYING) return false; + + if (mShakeIntensity > 0.0f) { + mShakeIntensity -= dt * 0.8f; // Decay over time + if (mShakeIntensity < 0.0f) mShakeIntensity = 0.0f; + } + + mClearedLinesThisTick.clear(); + + std::vector cells = getActiveCells(); + bool onGround = checkCollision(cells, 0, 1); + + if (onGround) { + if (!mIsLocking) { + mIsLocking = true; + mLockTimer = 0.0f; + } + + mLockTimer += dt; + // Lock delay threshold of 0.45 seconds + if (mLockTimer >= 0.45f) { + lockPiece(); + return true; + } + } else { + mIsLocking = false; + mFallTimer += dt; + + float fallDelay = getFallDelay(); + if (mFallTimer >= fallDelay) { + mFallTimer -= fallDelay; + mActiveY++; + } + } + return false; +} + +void Game::lockPiece() { + std::vector cells = getActiveCells(); + if (cells.empty()) return; + + mLastLockedCells = cells; + + for (const auto& p : cells) { + if (p.y >= 0 && p.y < BOARD_HEIGHT && p.x >= 0 && p.x < BOARD_WIDTH) { + mBoard[p.y][p.x] = mActiveType; + } + } + + flagLandSFX = true; + clearLines(); + spawnPiece(); +} + +void Game::clearLines() { + std::vector fullLines; + + for (int y = 0; y < BOARD_HEIGHT; ++y) { + bool full = true; + for (int x = 0; x < BOARD_WIDTH; ++x) { + if (mBoard[y][x] == PieceType::NONE) { + full = false; + break; + } + } + if (full) { + fullLines.push_back(y); + } + } + + if (fullLines.empty()) { + mCombo = -1; // reset combo + return; + } + + mClearedLinesThisTick = fullLines; // save for particle animations + mCombo++; + + // Scoring guideline + int baseScore = 0; + int cleared = fullLines.size(); + if (cleared == 1) { + baseScore = 100; + flagLineClearSFX = true; + mShakeIntensity = std::max(mShakeIntensity, 0.08f); + } else if (cleared == 2) { + baseScore = 300; + flagLineClearSFX = true; + mShakeIntensity = std::max(mShakeIntensity, 0.15f); + } else if (cleared == 3) { + baseScore = 500; + flagLineClearSFX = true; + mShakeIntensity = std::max(mShakeIntensity, 0.22f); + } else if (cleared >= 4) { + baseScore = 800; + flagTetrisClearSFX = true; // Boom! Tetris! + mShakeIntensity = std::max(mShakeIntensity, 0.40f); // Screen Rumble! + } + + mScore += baseScore * mLevel; + if (mCombo > 0) { + mScore += 50 * mCombo * mLevel; + } + + // Erase cleared lines and shift elements down + for (int lineY : fullLines) { + for (int currY = lineY; currY > 0; --currY) { + for (int x = 0; x < BOARD_WIDTH; ++x) { + mBoard[currY][x] = mBoard[currY - 1][x]; + } + } + // Top line cleared + for (int x = 0; x < BOARD_WIDTH; ++x) { + mBoard[0][x] = PieceType::NONE; + } + } + + mLinesCleared += cleared; + + // Level up logic (every 10 lines) + int nextLevel = (mLinesCleared / 10) + 1; + if (nextLevel > mLevel) { + mLevel = nextLevel; + flagLevelUpSFX = true; + } +} diff --git a/Game.hpp b/Game.hpp new file mode 100644 index 0000000..6f18d53 --- /dev/null +++ b/Game.hpp @@ -0,0 +1,189 @@ +#pragma once +#include +#include + +enum class PieceType { + I = 0, O, T, S, Z, J, L, NONE +}; + +struct Point { + int x; + int y; +}; + +// Coordinates for all 7 Tetromino types in their 4 rotation states (0, 1, 2, 3) +// Pivot anchor is (0,0). Screen space has Y-down, X-right. +inline const Point TETROMINO_CELLS[7][4][4] = { + // 0: I Piece + { + { {-1, 0}, {0, 0}, {1, 0}, {2, 0} }, // rot 0 + { {1, -1}, {1, 0}, {1, 1}, {1, 2} }, // rot 1 + { {-1, 1}, {0, 1}, {1, 1}, {2, 1} }, // rot 2 + { {0, -1}, {0, 0}, {0, 1}, {0, 2} } // rot 3 + }, + // 1: O Piece + { + { {0, 0}, {1, 0}, {0, 1}, {1, 1} }, // rot 0 + { {0, 0}, {1, 0}, {0, 1}, {1, 1} }, // rot 1 + { {0, 0}, {1, 0}, {0, 1}, {1, 1} }, // rot 2 + { {0, 0}, {1, 0}, {0, 1}, {1, 1} } // rot 3 + }, + // 2: T Piece + { + { {-1, 0}, {0, 0}, {1, 0}, {0, 1} }, // rot 0 + { {0, -1}, {0, 0}, {0, 1}, {-1, 0} }, // rot 1 + { {-1, 0}, {0, 0}, {1, 0}, {0, -1} }, // rot 2 + { {0, -1}, {0, 0}, {0, 1}, {1, 0} } // rot 3 + }, + // 3: S Piece + { + { {0, 0}, {1, 0}, {-1, 1}, {0, 1} }, // rot 0 + { {0, -1}, {0, 0}, {1, 0}, {1, 1} }, // rot 1 + { {0, -1}, {1, -1}, {-1, 0}, {0, 0} }, // rot 2 + { {-1, -1}, {-1, 0}, {0, 0}, {0, 1} } // rot 3 + }, + // 4: Z Piece + { + { {-1, 0}, {0, 0}, {0, 1}, {1, 1} }, // rot 0 + { {1, -1}, {1, 0}, {0, 0}, {0, 1} }, // rot 1 + { {-1, -1}, {0, -1}, {0, 0}, {1, 0} }, // rot 2 + { {0, -1}, {0, 0}, {-1, 0}, {-1, 1} } // rot 3 + }, + // 5: J Piece + { + { {-1, 0}, {0, 0}, {1, 0}, {-1, 1} }, // rot 0 + { {0, -1}, {0, 0}, {0, 1}, {-1, -1} }, // rot 1 + { {-1, 0}, {0, 0}, {1, 0}, {1, -1} }, // rot 2 + { {0, -1}, {0, 0}, {0, 1}, {1, 1} } // rot 3 + }, + // 6: L Piece + { + { {-1, 0}, {0, 0}, {1, 0}, {1, 1} }, // rot 0 + { {0, -1}, {0, 0}, {0, 1}, {-1, 1} }, // rot 1 + { {-1, 0}, {0, 0}, {1, 0}, {-1, -1} }, // rot 2 + { {0, -1}, {0, 0}, {0, 1}, {1, -1} } // rot 3 + } +}; + +enum class GameState { + START, + PLAYING, + PAUSED, + GAME_OVER +}; + +class Game { +public: + static constexpr int BOARD_WIDTH = 10; + static constexpr int BOARD_HEIGHT = 20; + + Game(); + ~Game() = default; + + void init(); + void reset(); + + // Core game loop update (handles gravity, lock delay, game states) + // Returns true if lines were cleared in this frame + bool update(float dt); + + // Gameplay commands (inputs) + bool moveLeft(); + bool moveRight(); + bool rotate(int dir); // dir = 1 (CW), -1 (CCW) + void softDrop(); + bool hardDrop(); // Returns true if piece locked + void holdPiece(); + + // Query states + GameState getState() const { return mState; } + void setState(GameState state) { mState = state; } + + int getScore() const { return mScore; } + int getLinesCleared() const { return mLinesCleared; } + int getLevel() const { return mLevel; } + int getCombo() const { return mCombo; } + + PieceType getCell(int x, int y) const { + if (x < 0 || x >= BOARD_WIDTH || y < 0 || y >= BOARD_HEIGHT) return PieceType::NONE; + return mBoard[y][x]; + } + + PieceType getActivePieceType() const { return mActiveType; } + int getActiveX() const { return mActiveX; } + int getActiveY() const { return mActiveY; } + int getActiveRotation() const { return mActiveRot; } + + // Relative cell coordinates of current falling block + std::vector getActiveCells() const; + + // Absolute cell coordinates of where active piece would land (ghost projection) + std::vector getGhostCells() const; + + PieceType getHoldPieceType() const { return mHoldType; } + bool canHold() const { return mCanHold; } + + std::vector getNextQueue() const { return mNextQueue; } + + // Inter-thread game sound effects triggers + bool flagMoveSFX = false; + bool flagRotateSFX = false; + bool flagLandSFX = false; + bool flagLineClearSFX = false; + bool flagTetrisClearSFX = false; + bool flagLevelUpSFX = false; + bool flagGameOverSFX = false; + + // Screen shake parameters + float getScreenShakeIntensity() const { return mShakeIntensity; } + void resetScreenShake() { mShakeIntensity = 0.0f; } + + // List of lines cleared on the current tick (for particle animations) + std::vector mClearedLinesThisTick; + std::vector mLastLockedCells; + +private: + GameState mState = GameState::START; + + // The grid: index y=0 is top, y=19 is bottom + PieceType mBoard[BOARD_HEIGHT][BOARD_WIDTH]; + + // Falling piece details + PieceType mActiveType = PieceType::NONE; + int mActiveX = 0; + int mActiveY = 0; + int mActiveRot = 0; + + // Hold slot details + PieceType mHoldType = PieceType::NONE; + bool mCanHold = true; + + // 7-piece bag randomizer + std::vector mNextQueue; + std::vector mBag; + std::mt19937 mRng; + + // Score metrics + int mScore = 0; + int mLinesCleared = 0; + int mLevel = 1; + int mCombo = -1; + + // Fall & Lock delay timers + float mFallTimer = 0.0f; + float mLockTimer = 0.0f; + bool mIsLocking = false; + float mShakeIntensity = 0.0f; + + float getFallDelay() const; + + void spawnPiece(); + void fillBag(); + PieceType popNextPiece(); + + bool checkCollision(const std::vector& cells, int dx, int dy) const; + bool fits(PieceType type, int rot, int cx, int cy) const; + + void lockPiece(); + void clearLines(); +}; diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..05540ee --- /dev/null +++ b/Makefile @@ -0,0 +1,28 @@ +CXX = g++ +CXXFLAGS = -std=c++17 -Wall -Wextra -O3 `sdl2-config --cflags` +LDFLAGS = `sdl2-config --libs` + +TARGET = cybermatris +OBJS = main.o Game.o Synth.o Renderer.o + +all: $(TARGET) + +$(TARGET): $(OBJS) + $(CXX) $(CXXFLAGS) -o $(TARGET) $(OBJS) $(LDFLAGS) + +main.o: main.cpp Game.hpp Synth.hpp Renderer.hpp Font.hpp + $(CXX) $(CXXFLAGS) -c main.cpp + +Game.o: Game.cpp Game.hpp + $(CXX) $(CXXFLAGS) -c Game.cpp + +Synth.o: Synth.cpp Synth.hpp + $(CXX) $(CXXFLAGS) -c Synth.cpp + +Renderer.o: Renderer.cpp Renderer.hpp Game.hpp Synth.hpp Font.hpp + $(CXX) $(CXXFLAGS) -c Renderer.cpp + +clean: + rm -f $(OBJS) $(TARGET) + +.PHONY: all clean diff --git a/Makefile.miyoo b/Makefile.miyoo new file mode 100644 index 0000000..3ecd109 --- /dev/null +++ b/Makefile.miyoo @@ -0,0 +1,109 @@ +# ============================================================ +# Makefile.miyoo - Cross-compile CyberMatris per Miyoo Mini Plus +# Toolchain: mini_toolchain-v1.0 (SigmaStar ARM) +# SDL2: sdl2-miyoo (build custom con backend MI GFX/AO) +# +# Usage: make -f Makefile.miyoo +# Deploy: make -f Makefile.miyoo deploy (richiede SSH su 10.0.0.199) +# ============================================================ + +TOOLCHAIN_ROOT = /home/enne2/dev/mini_toolchain-v1.0/mini +SDL2_ROOT = /home/enne2/dev/sdl2-miyoo + +CROSS = $(TOOLCHAIN_ROOT)/bin/arm-linux-gnueabihf- +CXX = $(CROSS)g++ +STRIP = $(CROSS)strip + +SDL2_INC = $(SDL2_ROOT)/sdl2/include +SDL2_LIB = $(SDL2_ROOT)/sdl2/build/.libs +SYSROOT = $(TOOLCHAIN_ROOT)/arm-buildroot-linux-gnueabihf/sysroot + +CXXFLAGS = -std=c++17 -O2 -Wall -Wextra \ + -I$(SDL2_INC) \ + -I$(SDL2_INC)/SDL2 \ + -DSDL_MAIN_HANDLED \ + -DMIYOO_BUILD + +# Linka SDL2 dinamicamente (il .so Miyoo custom include il backend MI_GFX/MI_AO) +# Le libmi_*.so sono proprietarie SigmaStar: esistono SOLO sul device in /config/lib/ +# Vengono risolte a runtime tramite LD_LIBRARY_PATH=.:/config/lib +# Usiamo --allow-shlib-undefined perché il linker host non le vede ma sul device ci sono. +LDFLAGS = -L$(SDL2_LIB) \ + -lSDL2 \ + -lpthread -lm -ldl -lrt \ + -Wl,-rpath,'$$ORIGIN' \ + -Wl,--allow-shlib-undefined + +TARGET = cybermatris_miyoo +OBJS = main.o Game.o Synth.o Renderer.o + +DEPLOY_IP = 10.0.0.199 +DEPLOY_USER = root +DEPLOY_PASS = +DEPLOY_DIR = /mnt/SDCARD/Roms/PORTS/Games/CyberMatris + +# Runtime libs da copiare nella stessa cartella del binario +RUNTIME_LIBS = \ + $(SDL2_ROOT)/sdl2/build/.libs/libSDL2-2.0.so.0 \ + $(SDL2_ROOT)/sdl2/build/.libs/libSDL2-2.0.so.0.18.2 \ + $(SDL2_ROOT)/sdl2/build/.libs/libEGL.so \ + $(SDL2_ROOT)/sdl2/build/.libs/libGLESv2.so \ + $(SYSROOT)/usr/lib/libjson-c.so.5 \ + $(SYSROOT)/usr/lib/libjson-c.so.5.1.0 + +.PHONY: all clean deploy + +all: $(TARGET) + +$(TARGET): $(OBJS) + $(CXX) $(CXXFLAGS) -o $@ $^ $(LDFLAGS) + $(STRIP) $@ + @echo "" + @echo ">>> Build completata: $(TARGET)" + @echo ">>> Dimensione: $$(du -sh $(TARGET) | cut -f1)" + +main.o: main.cpp Game.hpp Synth.hpp Renderer.hpp Font.hpp + $(CXX) $(CXXFLAGS) -c main.cpp + +Game.o: Game.cpp Game.hpp Synth.hpp + $(CXX) $(CXXFLAGS) -c Game.cpp + +Synth.o: Synth.cpp Synth.hpp + $(CXX) $(CXXFLAGS) -c Synth.cpp + +Renderer.o: Renderer.cpp Renderer.hpp Game.hpp Synth.hpp Font.hpp + $(CXX) $(CXXFLAGS) -c Renderer.cpp + +clean: + rm -f $(OBJS) $(TARGET) + +# ------------------------------------------------------- +# Crea il pacchetto di deploy (cartella con binario + libs + launcher) +# ------------------------------------------------------- +package: $(TARGET) + @mkdir -p dist_miyoo + @cp $(TARGET) dist_miyoo/ + @for lib in $(RUNTIME_LIBS); do \ + if [ -f "$$lib" ]; then cp "$$lib" dist_miyoo/; echo " Copiata: $$(basename $$lib)"; \ + else echo " WARN: $$lib non trovata, skip"; fi; \ + done + @cp launch_miyoo.sh dist_miyoo/ 2>/dev/null || true + @echo "" + @echo ">>> Pacchetto pronto in ./dist_miyoo/" + @ls -lh dist_miyoo/ + +# ------------------------------------------------------- +# Deploy via SSH + sshpass (password vuota) +# ------------------------------------------------------- +deploy: package + @echo ">>> Deploy su $(DEPLOY_USER)@$(DEPLOY_IP):$(DEPLOY_DIR)" + sshpass -p '$(DEPLOY_PASS)' ssh -o StrictHostKeyChecking=no \ + $(DEPLOY_USER)@$(DEPLOY_IP) "mkdir -p $(DEPLOY_DIR)" + sshpass -p '$(DEPLOY_PASS)' scp -o StrictHostKeyChecking=no \ + dist_miyoo/* $(DEPLOY_USER)@$(DEPLOY_IP):$(DEPLOY_DIR)/ + @echo ">>> Deploy completato!" + @echo "" + @echo ">>> Per lanciare sul device:" + @echo " ssh root@$(DEPLOY_IP)" + @echo " kill -STOP \`pidof MainUI\`" + @echo " cd $(DEPLOY_DIR) && LD_LIBRARY_PATH=.:/config/lib ./cybermatris_miyoo" diff --git a/Renderer.cpp b/Renderer.cpp new file mode 100644 index 0000000..1f9fec0 --- /dev/null +++ b/Renderer.cpp @@ -0,0 +1,845 @@ +#include "Renderer.hpp" +#include "Font.hpp" +#include +#include +#include +#include +#include + +// Playboard grid rendering layout variables +static constexpr int CELL_SIZE = 25; +static constexpr int BOARD_X = 275; +static constexpr int BOARD_Y = 50; +static constexpr int BOARD_W = 10 * CELL_SIZE; +static constexpr int BOARD_H = 20 * CELL_SIZE; + +// Sleek neon color constants +static const SDL_Color COLOR_CYAN = {0, 240, 255, 255}; +static const SDL_Color COLOR_PINK = {255, 0, 150, 255}; +static const SDL_Color COLOR_WHITE = {255, 255, 255, 255}; +static const SDL_Color COLOR_ORANGE = {255, 140, 0, 255}; +static const SDL_Color COLOR_RED = {245, 0, 50, 255}; + +// Helper function to draw a single pixel with optional alpha blending onto an RGBA32 surface +static inline void drawPixelSoftware(SDL_Surface* surface, int x, int y, SDL_Color color, bool blend) { + if (x < 0 || x >= surface->w || y < 0 || y >= surface->h) return; + + uint32_t* pixels = static_cast(surface->pixels); + int pitch = surface->pitch / 4; // 32-bit words per row + uint32_t& dstPixel = pixels[y * pitch + x]; + + if (!blend || color.a == 255) { + dstPixel = SDL_MapRGBA(surface->format, color.r, color.g, color.b, color.a); + } else if (color.a > 0) { + Uint8 dstR, dstG, dstB, dstA; + SDL_GetRGBA(dstPixel, surface->format, &dstR, &dstG, &dstB, &dstA); + + float alpha = color.a / 255.0f; + float invAlpha = 1.0f - alpha; + + Uint8 outR = static_cast(color.r * alpha + dstR * invAlpha); + Uint8 outG = static_cast(color.g * alpha + dstG * invAlpha); + Uint8 outB = static_cast(color.b * alpha + dstB * invAlpha); + Uint8 outA = static_cast(color.a + dstA * invAlpha); + + dstPixel = SDL_MapRGBA(surface->format, outR, outG, outB, outA); + } +} + +// Helper function to fill a rectangle with optional alpha blending on an RGBA32 surface +static void fillRectSoftware(SDL_Surface* surface, const SDL_Rect* rect, SDL_Color color, bool blend) { + SDL_Rect clippedRect; + SDL_Rect surfaceRect = {0, 0, surface->w, surface->h}; + if (!SDL_IntersectRect(rect, &surfaceRect, &clippedRect)) return; + + uint32_t* pixels = static_cast(surface->pixels); + int pitch = surface->pitch / 4; + + if (!blend || color.a == 255) { + uint32_t mappedColor = SDL_MapRGBA(surface->format, color.r, color.g, color.b, color.a); + for (int y = clippedRect.y; y < clippedRect.y + clippedRect.h; ++y) { + uint32_t* row = pixels + y * pitch; + std::fill(row + clippedRect.x, row + clippedRect.x + clippedRect.w, mappedColor); + } + } else if (color.a > 0) { + float alpha = color.a / 255.0f; + float invAlpha = 1.0f - alpha; + + for (int y = clippedRect.y; y < clippedRect.y + clippedRect.h; ++y) { + uint32_t* row = pixels + y * pitch; + for (int x = clippedRect.x; x < clippedRect.x + clippedRect.w; ++x) { + uint32_t& dstPixel = row[x]; + Uint8 dstR, dstG, dstB, dstA; + SDL_GetRGBA(dstPixel, surface->format, &dstR, &dstG, &dstB, &dstA); + + Uint8 outR = static_cast(color.r * alpha + dstR * invAlpha); + Uint8 outG = static_cast(color.g * alpha + dstG * invAlpha); + Uint8 outB = static_cast(color.b * alpha + dstB * invAlpha); + Uint8 outA = static_cast(color.a + dstA * invAlpha); + + dstPixel = SDL_MapRGBA(surface->format, outR, outG, outB, outA); + } + } + } +} + +// Helper function to draw a rectangle border with optional alpha blending on an RGBA32 surface +static void drawRectSoftware(SDL_Surface* surface, const SDL_Rect* rect, SDL_Color color, bool blend) { + // Top border + SDL_Rect top = {rect->x, rect->y, rect->w, 1}; + fillRectSoftware(surface, &top, color, blend); + // Bottom border + SDL_Rect bottom = {rect->x, rect->y + rect->h - 1, rect->w, 1}; + fillRectSoftware(surface, &bottom, color, blend); + // Left border + SDL_Rect left = {rect->x, rect->y, 1, rect->h}; + fillRectSoftware(surface, &left, color, blend); + // Right border + SDL_Rect right = {rect->x + rect->w - 1, rect->y, 1, rect->h}; + fillRectSoftware(surface, &right, color, blend); +} + +// Helper function to draw a line with optional alpha blending on an RGBA32 surface using Bresenham's algorithm +static void drawLineSoftware(SDL_Surface* surface, int x1, int y1, int x2, int y2, SDL_Color color, bool blend) { + // Handle trivial horizontal/vertical cases with fast fillRectSoftware + if (x1 == x2) { + int minY = std::min(y1, y2); + int maxY = std::max(y1, y2); + SDL_Rect rect = {x1, minY, 1, maxY - minY + 1}; + fillRectSoftware(surface, &rect, color, blend); + return; + } + if (y1 == y2) { + int minX = std::min(x1, x2); + int maxX = std::max(x1, x2); + SDL_Rect rect = {minX, y1, maxX - minX + 1, 1}; + fillRectSoftware(surface, &rect, color, blend); + return; + } + + int dx = std::abs(x2 - x1); + int dy = std::abs(y2 - y1); + int sx = (x1 < x2) ? 1 : -1; + int sy = (y1 < y2) ? 1 : -1; + int err = dx - dy; + + while (true) { + drawPixelSoftware(surface, x1, y1, color, blend); + if (x1 == x2 && y1 == y2) break; + int e2 = 2 * err; + if (e2 > -dy) { + err -= dy; + x1 += sx; + } + if (e2 < dx) { + err += dx; + y1 += sy; + } + } +} + +Renderer::Renderer() { + initStars(); +} + +Renderer::~Renderer() { + if (mFontTexture != nullptr) { + SDL_DestroyTexture(mFontTexture); + } + if (mFontSurface != nullptr) { + SDL_FreeSurface(mFontSurface); + } + if (mBackbufferSurface != nullptr) { + SDL_FreeSurface(mBackbufferSurface); + } + if (mScaledSurface != nullptr) { + SDL_FreeSurface(mScaledSurface); + } + if (mBackbufferTexture != nullptr) { + SDL_DestroyTexture(mBackbufferTexture); + } +} + +void Renderer::initStars() { + mStars.clear(); + for (int i = 0; i < 60; ++i) { + Star s; + s.x = static_cast(std::rand() % 800); + s.y = static_cast(std::rand() % 600); + s.speed = 10.0f + static_cast(std::rand() % 30); + s.size = 1.0f + static_cast(std::rand() % 3); + s.alpha = 50.0f + static_cast(std::rand() % 150); + mStars.push_back(s); + } +} + +bool Renderer::init(SDL_Renderer* renderer, int targetW, int targetH) { + mRenderer = renderer; + mTargetW = targetW; + mTargetH = targetH; + buildFontTexture(); + if (mFontSurface == nullptr) { + return false; + } + + // Create software backbuffer surface of 800x600 in RGBA32 format + mBackbufferSurface = SDL_CreateRGBSurfaceWithFormat(0, 800, 600, 32, SDL_PIXELFORMAT_RGBA32); + if (mBackbufferSurface == nullptr) { + return false; + } + +#ifdef MIYOO_BUILD + // On Miyoo, the maximum streaming texture size supported by the hardware/driver is 640x480. + // So we always scale the 800x600 backbuffer to a 640x480 surface/texture in RAM, + // and let the GPU/hardware scale the 640x480 texture to the actual window resolution (e.g., 752x560 on V4). + mScaledSurface = SDL_CreateRGBSurfaceWithFormat(0, 640, 480, 32, SDL_PIXELFORMAT_RGBA32); + if (mScaledSurface == nullptr) { + return false; + } + mBackbufferTexture = SDL_CreateTexture(mRenderer, SDL_PIXELFORMAT_RGBA32, SDL_TEXTUREACCESS_STREAMING, 640, 480); +#else + mBackbufferTexture = SDL_CreateTexture(mRenderer, SDL_PIXELFORMAT_RGBA32, SDL_TEXTUREACCESS_STREAMING, mTargetW, mTargetH); +#endif + + if (mBackbufferTexture == nullptr) { + return false; + } + + return true; +} + +void Renderer::buildFontTexture() { + // Lay out the 95-char font atlas in two rows to stay within the + // 640px texture width limit imposed by the Miyoo MI_GFX driver. + // Row 0: chars 0..47 (ASCII 32..79), Row 1: chars 48..94 (ASCII 80..126) + static constexpr int CHARS_PER_ROW = 48; + int atlasW = CHARS_PER_ROW * 8; // 384 pixels — fits in 640 + int atlasH = 16; // 2 rows of 8 pixels each + + SDL_Surface* surface = SDL_CreateRGBSurfaceWithFormat( + 0, atlasW, atlasH, 32, SDL_PIXELFORMAT_RGBA32); + if (surface == nullptr) return; + + SDL_LockSurface(surface); + uint32_t* pixels = (uint32_t*)surface->pixels; + int pitch = atlasW; // pixels per row (stride = atlasW * 4 bytes, but we use uint32*) + + for (int c = 0; c < 95; ++c) { + int row = c / CHARS_PER_ROW; + int col = c % CHARS_PER_ROW; + for (int r = 0; r < 8; ++r) { + uint8_t byte = Font::BITMAP[c][r]; + for (int bit = 0; bit < 8; ++bit) { + bool on = (byte >> (7 - bit)) & 1; + int px = col * 8 + bit; + int py = row * 8 + r; + pixels[py * pitch + px] = on ? 0xFFFFFFFF : 0x00000000; + } + } + } + + SDL_UnlockSurface(surface); + mFontSurface = surface; + mFontTexture = SDL_CreateTextureFromSurface(mRenderer, mFontSurface); + + if (mFontTexture != nullptr) { + SDL_SetTextureBlendMode(mFontTexture, SDL_BLENDMODE_BLEND); + } +} + +void Renderer::update(float dt) { + mTime += dt; + + // 1. Update Parallax Background Stars + for (auto& s : mStars) { + s.y += s.speed * dt; + if (s.y > 600.0f) { + s.y = 0.0f; + s.x = static_cast(std::rand() % 800); + } + } + + // 2. Update Physics Particles (gravitational debris) + for (size_t i = 0; i < mParticles.size();) { + Particle& p = mParticles[i]; + p.life -= p.decay * dt; + + if (p.life <= 0.0f) { + // Remove dead particles + mParticles[i] = mParticles.back(); + mParticles.pop_back(); + } else { + // Kinematic updates with gravity pulling down + p.x += p.vx * dt; + p.y += p.vy * dt; + p.vy += 320.0f * dt; // gravity + i++; + } + } +} + +SDL_Color Renderer::getPieceColor(PieceType type) { + switch (type) { + case PieceType::I: return {0, 240, 240, 255}; // Vibrant Cyan + case PieceType::O: return {240, 240, 0, 255}; // Vibrant Yellow + case PieceType::T: return {170, 0, 245, 255}; // Neon Purple + case PieceType::S: return {0, 240, 0, 255}; // Neon Green + case PieceType::Z: return {245, 0, 0, 255}; // Neon Red + case PieceType::J: return {0, 100, 245, 255}; // Deep Blue + case PieceType::L: return {245, 130, 0, 255}; // Bright Orange + default: return {0, 0, 0, 0}; + } +} + +void Renderer::drawBlock(int gridX, int gridY, PieceType type, bool isGhost, float alphaOverride) { + if (type == PieceType::NONE) return; + + int rx = BOARD_X + gridX * CELL_SIZE; + int ry = BOARD_Y + gridY * CELL_SIZE; + + SDL_Color color = getPieceColor(type); + + if (isGhost) { + // Draw pulsing outline for ghost pieces + float pulse = 0.5f + 0.3f * std::sin(mTime * 10.0f); + SDL_Color pulseCol = {color.r, color.g, color.b, static_cast(pulse * 255.0f * alphaOverride)}; + SDL_Rect rect = {rx + 1, ry + 1, CELL_SIZE - 2, CELL_SIZE - 2}; + drawRectSoftware(mBackbufferSurface, &rect, pulseCol, true); + return; + } + + // Neon beveled crystal shading style + // 1. Draw solid crystal core + SDL_Color coreCol = {color.r, color.g, color.b, static_cast(255 * alphaOverride)}; + SDL_Rect coreRect = {rx + 1, ry + 1, CELL_SIZE - 2, CELL_SIZE - 2}; + fillRectSoftware(mBackbufferSurface, &coreRect, coreCol, true); + + // 2. Draw glossy 3D inner reflection core + SDL_Color lighterColor = { + static_cast(std::min(color.r + 60, 255)), + static_cast(std::min(color.g + 60, 255)), + static_cast(std::min(color.b + 60, 255)), + 255 + }; + SDL_Color reflectionCol = {lighterColor.r, lighterColor.g, lighterColor.b, static_cast(200 * alphaOverride)}; + SDL_Rect innerRect = {rx + 3, ry + 3, CELL_SIZE - 6, CELL_SIZE - 6}; + fillRectSoftware(mBackbufferSurface, &innerRect, reflectionCol, true); + + // 3. Highlight top-left border (specular light sheen) + SDL_Color highlightCol = {255, 255, 255, static_cast(180 * alphaOverride)}; + drawLineSoftware(mBackbufferSurface, rx + 1, ry + 1, rx + CELL_SIZE - 2, ry + 1, highlightCol, true); + drawLineSoftware(mBackbufferSurface, rx + 1, ry + 1, rx + 1, ry + CELL_SIZE - 2, highlightCol, true); + + // 4. Shade bottom-right border (3D shadow depth) + SDL_Color shadowCol = {0, 0, 0, static_cast(130 * alphaOverride)}; + drawLineSoftware(mBackbufferSurface, rx + 1, ry + CELL_SIZE - 1, rx + CELL_SIZE - 1, ry + CELL_SIZE - 1, shadowCol, true); + drawLineSoftware(mBackbufferSurface, rx + CELL_SIZE - 1, ry + 1, rx + CELL_SIZE - 1, ry + CELL_SIZE - 1, shadowCol, true); +} + +void Renderer::drawGrid(const Game& game) { + // Subtle dark blue/gray playboard background + SDL_Color boardCol = {15, 17, 24, 255}; + SDL_Rect boardBG = {BOARD_X, BOARD_Y, BOARD_W, BOARD_H}; + fillRectSoftware(mBackbufferSurface, &boardBG, boardCol, true); + + // Draw grid lines + SDL_Color gridLineCol = {30, 34, 46, 120}; + for (int x = 1; x < 10; ++x) { + drawLineSoftware(mBackbufferSurface, BOARD_X + x * CELL_SIZE, BOARD_Y, BOARD_X + x * CELL_SIZE, BOARD_Y + BOARD_H, gridLineCol, true); + } + for (int y = 1; y < 20; ++y) { + drawLineSoftware(mBackbufferSurface, BOARD_X, BOARD_Y + y * CELL_SIZE, BOARD_X + BOARD_W, BOARD_Y + y * CELL_SIZE, gridLineCol, true); + } + + // Draw locked blocks on the board + for (int y = 0; y < 20; ++y) { + for (int x = 0; x < 10; ++x) { + PieceType cell = game.getCell(x, y); + if (cell != PieceType::NONE) { + // If a line is cleared, skip drawing it here so it flashes white under the particles + if (std::find(game.mClearedLinesThisTick.begin(), game.mClearedLinesThisTick.end(), y) != game.mClearedLinesThisTick.end()) { + continue; + } + drawBlock(x, y, cell); + } + } + } + + // Draw dynamic glowing borders around the play board (cyberpunk look) + float pulse = 0.8f + 0.2f * std::sin(mTime * 4.0f); + SDL_Color borderCol = {0, 150, 255, static_cast(255 * pulse)}; + + // Outer border outline layers + for (int i = 0; i < 3; ++i) { + SDL_Rect border = {BOARD_X - i, BOARD_Y - i, BOARD_W + i * 2, BOARD_H + i * 2}; + drawRectSoftware(mBackbufferSurface, &border, borderCol, true); + } +} + +void Renderer::drawText(const std::string& text, int x, int y, int scale, SDL_Color color, bool center, bool glow) { + if (mFontSurface == nullptr) return; + + // Font atlas layout: 48 chars per row, 2 rows of 8px each. + static constexpr int CHARS_PER_ROW = 48; + + int totalWidth = text.length() * 8 * scale; + int startX = center ? x - totalWidth / 2 : x; + + SDL_SetSurfaceBlendMode(mFontSurface, SDL_BLENDMODE_BLEND); + + auto charSrc = [](char c) -> SDL_Rect { + int idx = (c >= 32 && c <= 126) ? (c - 32) : 0; + int row = idx / CHARS_PER_ROW; + int col = idx % CHARS_PER_ROW; + return SDL_Rect{ col * 8, row * 8, 8, 8 }; + }; + + // Optional black text outline to simulate glow/shadow + if (glow) { + SDL_SetSurfaceColorMod(mFontSurface, 0, 0, 0); + SDL_SetSurfaceAlphaMod(mFontSurface, 180); + for (int dx = -1; dx <= 1; ++dx) { + for (int dy = -1; dy <= 1; ++dy) { + if (dx == 0 && dy == 0) continue; + int curX = startX; + for (char c : text) { + if (c >= 32 && c <= 126) { + SDL_Rect src = charSrc(c); + SDL_Rect dest = { curX + dx * scale, y + dy * scale, 8 * scale, 8 * scale }; + SDL_BlitScaled(mFontSurface, &src, mBackbufferSurface, &dest); + } + curX += 8 * scale; + } + } + } + } + + // Main text render + SDL_SetSurfaceColorMod(mFontSurface, color.r, color.g, color.b); + SDL_SetSurfaceAlphaMod(mFontSurface, color.a); + + int curX = startX; + for (char c : text) { + if (c >= 32 && c <= 126) { + SDL_Rect src = charSrc(c); + SDL_Rect dest = { curX, y, 8 * scale, 8 * scale }; + SDL_BlitScaled(mFontSurface, &src, mBackbufferSurface, &dest); + } + curX += 8 * scale; + } +} + +void Renderer::drawUI(const Game& game) { + // 1. Title + float titlePulse = std::sin(mTime * 3.0f); + SDL_Color dynamicColor = (titlePulse >= 0.0f) ? COLOR_CYAN : COLOR_PINK; + drawText("CYBERMATRIS", 400, 15, 3, dynamicColor, true, true); + + // 2. Hold Box Panel (Left Side) + drawText("HOLD", 140, 50, 2, COLOR_CYAN, true, true); + SDL_Color panelBorderCol = {0, 150, 255, 120}; + SDL_Rect holdRect = {40, 70, 200, 110}; + drawRectSoftware(mBackbufferSurface, &holdRect, panelBorderCol, true); + + PieceType holdPiece = game.getHoldPieceType(); + if (holdPiece != PieceType::NONE) { + int typeIdx = static_cast(holdPiece); + SDL_Color pColor = getPieceColor(holdPiece); + + // Compute offsets to center pieces perfectly + int offX = -12; + int offY = -12; + + for (int i = 0; i < 4; ++i) { + Point p = TETROMINO_CELLS[typeIdx][0][i]; + int rx = 40 + 100 + offX + p.x * CELL_SIZE; + int ry = 70 + 55 + offY + p.y * CELL_SIZE; + + // Draw beveled block + SDL_Color blockCol = {pColor.r, pColor.g, pColor.b, 255}; + SDL_Rect bRect = {rx + 1, ry + 1, CELL_SIZE - 2, CELL_SIZE - 2}; + fillRectSoftware(mBackbufferSurface, &bRect, blockCol, true); + + SDL_Color highlightCol = {255, 255, 255, 160}; + drawLineSoftware(mBackbufferSurface, rx + 1, ry + 1, rx + CELL_SIZE - 2, ry + 1, highlightCol, true); + drawLineSoftware(mBackbufferSurface, rx + 1, ry + 1, rx + 1, ry + CELL_SIZE - 2, highlightCol, true); + + SDL_Color shadowCol = {0, 0, 0, 110}; + drawLineSoftware(mBackbufferSurface, rx + 1, ry + CELL_SIZE - 1, rx + CELL_SIZE - 1, ry + CELL_SIZE - 1, shadowCol, true); + drawLineSoftware(mBackbufferSurface, rx + CELL_SIZE - 1, ry + 1, rx + CELL_SIZE - 1, ry + CELL_SIZE - 1, shadowCol, true); + } + } + + // 3. Stats Dashboard Panel (Left Side, below Hold) + drawText("STATS", 140, 205, 2, COLOR_CYAN, true, true); + SDL_Rect statsRect = {40, 225, 200, 325}; + drawRectSoftware(mBackbufferSurface, &statsRect, panelBorderCol, true); + + // Render text metrics inside dashboard + drawText("SCORE", 140, 245, 1, COLOR_CYAN, true, false); + std::stringstream ssScore; + ssScore << std::setw(6) << std::setfill('0') << game.getScore(); + drawText(ssScore.str(), 140, 260, 2, COLOR_ORANGE, true, true); + + drawText("LEVEL", 140, 305, 1, COLOR_CYAN, true, false); + drawText(std::to_string(game.getLevel()), 140, 320, 2, COLOR_WHITE, true, true); + + drawText("LINES", 140, 365, 1, COLOR_CYAN, true, false); + drawText(std::to_string(game.getLinesCleared()), 140, 380, 2, COLOR_WHITE, true, true); + + // Show Combo notifier if positive + if (game.getCombo() > 0) { + float comboPulse = std::sin(mTime * 15.0f) * 0.5f + 0.5f; + SDL_Color cColor = {255, static_cast(100 + 155 * comboPulse), 0, 255}; + + std::string comboStr = "COMBO X" + std::to_string(game.getCombo() + 1); + drawText(comboStr, 140, 440, 2, cColor, true, true); + drawText("Pulsing!", 140, 470, 1, COLOR_PINK, true, false); + } else { + drawText("CHIPTUNE", 140, 445, 1, COLOR_PINK, true, false); + drawText("SYNTH ACTIVE", 140, 465, 1, COLOR_CYAN, true, false); + drawText("4-CH MONSTER", 140, 485, 1, COLOR_WHITE, true, false); + } + + // 4. Next Pieces Queue Box (Right Side) + drawText("NEXT", 660, 50, 2, COLOR_CYAN, true, true); + SDL_Rect nextRect = {560, 70, 200, 305}; + drawRectSoftware(mBackbufferSurface, &nextRect, panelBorderCol, true); + + // Draw next 3 pieces + auto nextQueue = game.getNextQueue(); + for (int n = 0; n < 3; ++n) { + if (n >= (int)nextQueue.size()) break; + PieceType nPiece = nextQueue[n]; + + int typeIdx = static_cast(nPiece); + SDL_Color pColor = getPieceColor(nPiece); + + int offX = -12; + int offY = -12; + + // Render mini blocks for each next queue element in slots + for (int i = 0; i < 4; ++i) { + Point p = TETROMINO_CELLS[typeIdx][0][i]; + int rx = 560 + 100 + offX + p.x * CELL_SIZE; + int ry = 70 + 60 + n * 95 + offY + p.y * CELL_SIZE; + + SDL_Color blockCol = {pColor.r, pColor.g, pColor.b, 255}; + SDL_Rect bRect = {rx + 1, ry + 1, CELL_SIZE - 2, CELL_SIZE - 2}; + fillRectSoftware(mBackbufferSurface, &bRect, blockCol, true); + + SDL_Color highlightCol = {255, 255, 255, 160}; + drawLineSoftware(mBackbufferSurface, rx + 1, ry + 1, rx + CELL_SIZE - 2, ry + 1, highlightCol, true); + drawLineSoftware(mBackbufferSurface, rx + 1, ry + 1, rx + 1, ry + CELL_SIZE - 2, highlightCol, true); + + SDL_Color shadowCol = {0, 0, 0, 110}; + drawLineSoftware(mBackbufferSurface, rx + 1, ry + CELL_SIZE - 1, rx + CELL_SIZE - 1, ry + CELL_SIZE - 1, shadowCol, true); + drawLineSoftware(mBackbufferSurface, rx + CELL_SIZE - 1, ry + 1, rx + CELL_SIZE - 1, ry + CELL_SIZE - 1, shadowCol, true); + } + } +} + +void Renderer::drawVisualizer(Synth& synth, int x, int y, int w, int h) { + // 1. Draw outer pane container + SDL_Color containerCol = {0, 150, 255, 120}; + SDL_Rect container = {x, y, w, h}; + drawRectSoftware(mBackbufferSurface, &container, containerCol, true); + + // Subtle Visualizer label + drawText("AUDIO OSCILLOSCOPE", x + w / 2, y + 10, 1, {0, 240, 255, 255}, true, false); + + // Fetch the live stereo buffer copies + auto buffer = synth.getVisualizerBuffer(); + if (buffer.empty()) return; + + int halfH = h / 2; + int centerY = y + halfH + 10; + int dataW = w - 20; + int dataX = x + 10; + + // 2. Draw 12 spectrum equalizer bar segments in background + int numBars = 12; + int barW = (dataW - (numBars - 1) * 3) / numBars; + int samplesPerBar = buffer.size() / numBars; + + for (int b = 0; b < numBars; ++b) { + // Calculate dynamic energy (Root Mean Square average amplitude) of segment + float rmsSum = 0.0f; + for (int s = 0; s < samplesPerBar; ++s) { + float val = buffer[b * samplesPerBar + s]; + rmsSum += val * val; + } + float rms = std::sqrt(rmsSum / samplesPerBar); + + // Smooth scaling factor + float barHeight = rms * (h - 60) * 2.8f; + barHeight = std::clamp(barHeight, 2.0f, static_cast(h - 50)); + + // Pulsing color cycle: fades from hot purple to high green + SDL_Color barCol = { + static_cast(std::min(rms * 900.0f, 255.0f)), + static_cast(180 - std::min(rms * 300.0f, 180.0f)), + 245, + 70 + }; + + SDL_Rect barRect = { + dataX + b * (barW + 3), + y + h - 15 - static_cast(barHeight), + barW, + static_cast(barHeight) + }; + fillRectSoftware(mBackbufferSurface, &barRect, barCol, true); + } + + // 3. Draw neon green oscilloscope line connecting points + SDL_Color waveCol = {0, 255, 120, 255}; + + int prevX = dataX; + int prevY = centerY; + int step = buffer.size() / dataW; + if (step <= 0) step = 1; + + for (int i = 0; i < dataW; ++i) { + int idx = i * step; + if (idx >= (int)buffer.size()) break; + + float sample = buffer[idx]; + int px = dataX + i; + + // Amplify wave height + int py = centerY + static_cast(sample * halfH * 1.5f); + py = std::clamp(py, y + 25, y + h - 10); + + if (i > 0) { + drawLineSoftware(mBackbufferSurface, prevX, prevY, px, py, waveCol, true); + } + prevX = px; + prevY = py; + } +} + +void Renderer::spawnLineClearParticles(const Game& game) { + // When lines clear, shatter blocks into glowing spark bursts! + + for (int y : game.mClearedLinesThisTick) { + // For every block across the line + for (int x = 0; x < 10; ++x) { + PieceType pType = game.getCell(x, y); + if (pType == PieceType::NONE) pType = PieceType::I; // fallback + + SDL_Color color = getPieceColor(pType); + int rx = BOARD_X + x * CELL_SIZE + CELL_SIZE / 2; + int ry = BOARD_Y + y * CELL_SIZE + CELL_SIZE / 2; + + // Spawn 5 explosive sparks per block + for (int p = 0; p < 5; ++p) { + Particle part; + part.x = static_cast(rx); + part.y = static_cast(ry); + + // Random explosive velocity vectors + float angle = static_cast(std::rand() % 360) * (3.14159f / 180.0f); + float speed = 50.0f + static_cast(std::rand() % 220); + part.vx = std::cos(angle) * speed; + part.vy = std::sin(angle) * speed - 50.0f; // slight upwards blast bias + + part.life = 1.0f; + part.decay = 1.2f + static_cast(std::rand() % 10) * 0.1f; // decay ~0.8s + part.size = 2.0f + static_cast(std::rand() % 4); + part.color = color; + + mParticles.push_back(part); + } + } + } +} + +void Renderer::spawnLandDustParticles(int gridX, int gridY, PieceType type) { + if (type == PieceType::NONE) return; + + SDL_Color color = {230, 235, 255, 200}; // light grayish white puff + + int rx = BOARD_X + gridX * CELL_SIZE + CELL_SIZE / 2; + int ry = BOARD_Y + (gridY + 1) * CELL_SIZE; // spawn right at contact base + + // Spawn 8 tiny dust puff particles drifting left and right + for (int i = 0; i < 8; ++i) { + Particle p; + p.x = static_cast(rx + (std::rand() % CELL_SIZE) - CELL_SIZE / 2); + p.y = static_cast(ry - 2); + + p.vx = static_cast((std::rand() % 100) - 50); // drift sideways + p.vy = -10.0f - static_cast(std::rand() % 30); // slight upwards float + + p.life = 1.0f; + p.decay = 2.0f + static_cast(std::rand() % 10) * 0.2f; // quick decay ~0.4s + p.size = 2.0f + static_cast(std::rand() % 3); + p.color = color; + + mParticles.push_back(p); + } +} + +void Renderer::render(const Game& game, Synth& synth) { + if (mBackbufferSurface == nullptr) return; + + // 1. Clear Screen with deep cosmic space black + SDL_Color clearCol = {10, 11, 16, 255}; + SDL_Rect bgRect = {0, 0, 800, 600}; + fillRectSoftware(mBackbufferSurface, &bgRect, clearCol, false); + + // 2. Render drifting parallax background stars + for (const auto& s : mStars) { + SDL_Color starCol = {255, 255, 255, static_cast(s.alpha)}; + SDL_Rect starRect = { + static_cast(s.x), + static_cast(s.y), + static_cast(s.size), + static_cast(s.size) + }; + fillRectSoftware(mBackbufferSurface, &starRect, starCol, true); + } + + // Apply decayed screenshake camera offsets if any + int camDX = 0; + int camDY = 0; + mShakeIntensity = game.getScreenShakeIntensity(); + + if (mShakeIntensity > 0.01f) { + camDX = static_cast(((float)std::rand() / RAND_MAX * 2.0f - 1.0f) * mShakeIntensity * 15.0f); + camDY = static_cast(((float)std::rand() / RAND_MAX * 2.0f - 1.0f) * mShakeIntensity * 15.0f); + } + + // 3. Render the dynamic cyberpunk grid and locked board blocks + drawGrid(game); + + // 4. Render Active Tetromino (glowing crystal style) + PieceType activeType = game.getActivePieceType(); + if (activeType != PieceType::NONE) { + // A. Draw Ghost projection piece first (pulsing neon wireframe) + auto ghostCells = game.getGhostCells(); + for (const auto& p : ghostCells) { + drawBlock(p.x, p.y, activeType, true); + } + + // B. Draw Active Piece blocks + auto activeCells = game.getActiveCells(); + for (const auto& p : activeCells) { + drawBlock(p.x, p.y, activeType); + } + } + + // 5. Render sidebars, score gauge, next and hold compartments + drawUI(game); + + // 6. Render real-time audio visualizer oscilloscope panel + drawVisualizer(synth, 560, 390, 200, 160); + + // 7. Render physics particles (debris sparks) + for (const auto& p : mParticles) { + SDL_Color pCol = {p.color.r, p.color.g, p.color.b, static_cast(p.life * 255.0f)}; + SDL_Rect pRect = { + static_cast(p.x - p.size / 2), + static_cast(p.y - p.size / 2), + static_cast(p.size), + static_cast(p.size) + }; + fillRectSoftware(mBackbufferSurface, &pRect, pCol, true); + } + + // 8. Render Game Over / Start Banner overlay + if (game.getState() == GameState::GAME_OVER) { + // Translucent overlay shroud + SDL_Color overlayCol = {10, 10, 15, 200}; + SDL_Rect overlay = {0, 0, 800, 600}; + fillRectSoftware(mBackbufferSurface, &overlay, overlayCol, true); + + float flash = std::sin(mTime * 6.0f) * 0.5f + 0.5f; + SDL_Color flashRed = {255, static_cast(flash * 100), static_cast(flash * 50), 255}; + + drawText("GAME OVER", 400, 220, 4, flashRed, true, true); + + std::stringstream ssFinalScore; + ssFinalScore << "FINAL SCORE " << game.getScore(); + drawText(ssFinalScore.str(), 400, 280, 2, COLOR_CYAN, true, false); + + drawText("PRESS R TO RESTART", 400, 350, 2, COLOR_WHITE, true, true); + drawText("OR ESC TO QUIT", 400, 390, 1, {180, 180, 200, 255}, true, false); + } else if (game.getState() == GameState::START) { + SDL_Color overlayCol = {8, 9, 13, 230}; + SDL_Rect overlay = {0, 0, 800, 600}; + fillRectSoftware(mBackbufferSurface, &overlay, overlayCol, true); + + // Blinking start cue + float blink = std::sin(mTime * 5.0f) * 0.5f + 0.5f; + SDL_Color cueColor = {static_cast(180 + 75 * blink), static_cast(200 + 55 * blink), 255, 255}; + + drawText("CYBERMATRIS", 400, 180, 5, COLOR_PINK, true, true); + drawText("RETROWAVE PROCEDURAL TETRIS", 400, 240, 1, COLOR_CYAN, true, false); + + // Game features overview + drawText("PROCEDURAL CHIPTUNE MUSIC", 400, 300, 1, COLOR_WHITE, true, false); + drawText("NEON PARTICLE SHATTER EFFECTS", 400, 320, 1, COLOR_WHITE, true, false); + drawText("REAL-TIME AUDIO OSCILLOSCOPE", 400, 340, 1, COLOR_WHITE, true, false); + + drawText("PRESS ENTER TO START", 400, 420, 2, cueColor, true, true); + + // Controls list + drawText("CONTROLS", 400, 475, 1, COLOR_CYAN, true, false); + drawText("LEFT RIGHT MOVEMENT DOWN SOFT DROP SPACE HARD DROP", 400, 495, 1, {170, 180, 200, 255}, true, false); + drawText("UP X ROTATE CLOCKWISE Z ROTATE COUNTER C HOLD PIECE", 400, 515, 1, {170, 180, 200, 255}, true, false); + drawText("P PAUSE GAME R RESTART GAME", 400, 535, 1, {170, 180, 200, 255}, true, false); + + drawText("CREATED BY MATTEO BENEDETTO (ENNE2)", 400, 575, 1, {255, 200, 0, 255}, true, false); + } else if (game.getState() == GameState::PAUSED) { + SDL_Color overlayCol = {10, 11, 16, 150}; + SDL_Rect overlay = {0, 0, 800, 600}; + fillRectSoftware(mBackbufferSurface, &overlay, overlayCol, true); + + float flash = std::sin(mTime * 4.0f) * 0.5f + 0.5f; + SDL_Color flashCyan = {static_cast(flash * 100), 220, 255, 255}; + + drawText("PAUSED", 400, 260, 4, flashCyan, true, true); + drawText("PRESS P TO RESUME", 400, 310, 2, COLOR_WHITE, true, false); + } + + // 9. Upload backbuffer surface to streaming texture on GPU +#ifdef MIYOO_BUILD + // Scale the 800x600 software backbuffer to target screen size in software RAM + SDL_BlitScaled(mBackbufferSurface, nullptr, mScaledSurface, nullptr); + SDL_UpdateTexture(mBackbufferTexture, nullptr, mScaledSurface->pixels, mScaledSurface->pitch); +#else + SDL_UpdateTexture(mBackbufferTexture, nullptr, mBackbufferSurface->pixels, mBackbufferSurface->pitch); +#endif + + // Clear the hardware renderer (Miyoo requires this before rendering copy) + SDL_RenderClear(mRenderer); + + // Draw the texture containing our fully rendered software scene + // If screenshake is active, apply the shake offset directly to the destination rectangle! +#ifdef MIYOO_BUILD + SDL_Rect destRect = { + static_cast(camDX * static_cast(mTargetW) / 800.0f), + static_cast(camDY * static_cast(mTargetH) / 600.0f), + mTargetW, + mTargetH + }; +#else + SDL_Rect destRect = { + camDX, + camDY, + mTargetW, + mTargetH + }; +#endif + SDL_RenderCopy(mRenderer, mBackbufferTexture, nullptr, &destRect); + + // Present the frame! + SDL_RenderPresent(mRenderer); +} + +void Renderer::takeScreenshot(const std::string& filename) { + if (mBackbufferSurface == nullptr) return; + SDL_SaveBMP(mBackbufferSurface, filename.c_str()); +} diff --git a/Renderer.hpp b/Renderer.hpp new file mode 100644 index 0000000..26d9ce7 --- /dev/null +++ b/Renderer.hpp @@ -0,0 +1,74 @@ +#pragma once +#include +#include +#include +#include "Game.hpp" +#include "Synth.hpp" + +struct Particle { + float x = 0.0f; + float y = 0.0f; + float vx = 0.0f; + float vy = 0.0f; + float life = 1.0f; // 1.0 down to 0.0 + float decay = 1.0f; // decay rate per second + float size = 4.0f; + SDL_Color color = {255, 255, 255, 255}; +}; + +struct Star { + float x = 0.0f; + float y = 0.0f; + float speed = 0.0f; + float size = 0.0f; + float alpha = 0.0f; +}; + +class Renderer { +public: + Renderer(); + ~Renderer(); + + bool init(SDL_Renderer* renderer, int targetW, int targetH); + void update(float dt); + + // Master rendering routine + void render(const Game& game, Synth& synth); + + // Spawn effects + void spawnLineClearParticles(const Game& game); + void spawnLandDustParticles(int gridX, int gridY, PieceType type); + + // Screenshot API + void takeScreenshot(const std::string& filename); + + private: + SDL_Renderer* mRenderer = nullptr; + SDL_Texture* mFontTexture = nullptr; + SDL_Surface* mFontSurface = nullptr; + SDL_Surface* mBackbufferSurface = nullptr; + SDL_Surface* mScaledSurface = nullptr; + SDL_Texture* mBackbufferTexture = nullptr; + + int mTargetW = 800; + int mTargetH = 600; + float mTime = 0.0f; + float mShakeIntensity = 0.0f; + + // Drifting starfield + std::vector mStars; + std::vector mParticles; + + void initStars(); + void buildFontTexture(); + + // Drawing helpers + void drawText(const std::string& text, int x, int y, int scale, SDL_Color color, bool center = false, bool glow = false); + void drawBlock(int gridX, int gridY, PieceType type, bool isGhost = false, float alphaOverride = 1.0f); + void drawGrid(const Game& game); + void drawUI(const Game& game); + void drawVisualizer(Synth& synth, int x, int y, int w, int h); + + // Helpers + SDL_Color getPieceColor(PieceType type); +}; diff --git a/Synth.cpp b/Synth.cpp new file mode 100644 index 0000000..444509a --- /dev/null +++ b/Synth.cpp @@ -0,0 +1,802 @@ +#include "Synth.hpp" +#include +#include +#include + +#ifndef M_PI +#define M_PI 3.14159265358979323846 +#endif + +// Midi note number to frequency conversion helper +static inline float midiToFreq(int note) { + if (note <= 0) return 0.0f; + return 440.0f * std::pow(2.0f, (note - 69.0f) / 12.0f); +} + +Synth::Synth() { + for (int i = 0; i < VIS_BUFFER_SIZE; ++i) { + mVisBuffer[i] = 0.0f; + } + initSequencer(); +} + +Synth::~Synth() { + if (mAudioDevice > 0) { + SDL_CloseAudioDevice(mAudioDevice); + } +} + +bool Synth::init() { + SDL_AudioSpec wanted, obtained; + SDL_zero(wanted); + wanted.freq = 44100; + wanted.format = AUDIO_S16SYS; // Signed 16-bit, native endian + wanted.channels = 2; // Stereo +#ifdef MIYOO_BUILD + wanted.samples = 1024; // Larger buffer size to satisfy Miyoo MI_AO requirements +#else + wanted.samples = 256; // Ultra low latency +#endif + wanted.callback = &Synth::audioCallbackWrapper; + wanted.userdata = this; + + mAudioDevice = SDL_OpenAudioDevice(nullptr, 0, &wanted, &obtained, 0); + if (mAudioDevice == 0) { + return false; + } + + // Start playing silence initially + SDL_PauseAudioDevice(mAudioDevice, 0); + return true; +} + +void Synth::playBGM(bool play) { + std::lock_guard lock(mMutex); + mBGMEnabled = play; + if (!play) { + // Stop music voices (0, 1, 2, 3) + for (int i = 0; i < 4; ++i) { + mVoices[i].active = false; + } + } +} + +void Synth::stopAllSFX() { + std::lock_guard lock(mMutex); + for (int i = 4; i < NUM_VOICES; ++i) { + mVoices[i].active = false; + } +} + +void Synth::shufflePlaylist() { + mPlaylist[0] = 0; // Always anchor with original theme + int variants[] = {1, 2, 3, 4, 5}; + std::random_shuffle(std::begin(variants), std::end(variants)); + mPlaylist[1] = variants[0]; + mPlaylist[2] = variants[1]; + mPlaylist[3] = variants[2]; +} + +void Synth::initSequencer() { + mSecsPerTick = 60.0f / (mBPM * 2.0f); + mTickTimer = 0.0f; + mCurrentTick = 0; + + for (int p = 0; p < NUM_PATTERNS; ++p) { + for (int i = 0; i < 64; ++i) { + mPatternLead[p][i] = 0; + mPatternBass[p][i] = 0; + } + for (int b = 0; b < 8; ++b) { + mPatternChords[p][b] = ChordType::AM; + } + } + + // PATTERN 0: Original Korobeiniki + mPatternLead[0][0] = 76; mPatternLead[0][1] = 76; mPatternLead[0][2] = 71; mPatternLead[0][3] = 72; + mPatternLead[0][4] = 74; mPatternLead[0][5] = 74; mPatternLead[0][6] = 72; mPatternLead[0][7] = 71; + mPatternLead[0][8] = 69; mPatternLead[0][9] = 69; mPatternLead[0][10] = 69; mPatternLead[0][11] = 72; + mPatternLead[0][12] = 76; mPatternLead[0][13] = 76; mPatternLead[0][14] = 74; mPatternLead[0][15] = 72; + mPatternLead[0][16] = 71; mPatternLead[0][17] = 71; mPatternLead[0][18] = 71; mPatternLead[0][19] = 72; + mPatternLead[0][20] = 74; mPatternLead[0][21] = 74; mPatternLead[0][22] = 76; mPatternLead[0][23] = 76; + mPatternLead[0][24] = 72; mPatternLead[0][25] = 72; mPatternLead[0][26] = 69; mPatternLead[0][27] = 69; + mPatternLead[0][28] = 69; + mPatternLead[0][32] = 74; mPatternLead[0][33] = 74; mPatternLead[0][34] = 74; mPatternLead[0][35] = 77; + mPatternLead[0][36] = 81; mPatternLead[0][37] = 81; mPatternLead[0][38] = 79; mPatternLead[0][39] = 77; + mPatternLead[0][40] = 76; mPatternLead[0][41] = 76; mPatternLead[0][42] = 72; mPatternLead[0][43] = 72; + mPatternLead[0][44] = 76; mPatternLead[0][45] = 76; mPatternLead[0][46] = 74; mPatternLead[0][47] = 72; + mPatternLead[0][48] = 71; mPatternLead[0][49] = 71; mPatternLead[0][50] = 71; mPatternLead[0][51] = 72; + mPatternLead[0][52] = 74; mPatternLead[0][53] = 74; mPatternLead[0][54] = 76; mPatternLead[0][55] = 76; + mPatternLead[0][56] = 72; mPatternLead[0][57] = 72; mPatternLead[0][58] = 69; mPatternLead[0][59] = 69; + mPatternLead[0][60] = 69; + + mPatternBass[0][0] = 45; mPatternBass[0][2] = 57; mPatternBass[0][4] = 45; mPatternBass[0][6] = 57; + mPatternBass[0][8] = 45; mPatternBass[0][10] = 57; mPatternBass[0][12] = 45; mPatternBass[0][14] = 57; + mPatternBass[0][16] = 40; mPatternBass[0][18] = 52; mPatternBass[0][20] = 40; mPatternBass[0][22] = 52; + mPatternBass[0][24] = 45; mPatternBass[0][26] = 57; mPatternBass[0][28] = 45; mPatternBass[0][30] = 45; + mPatternBass[0][32] = 50; mPatternBass[0][34] = 62; mPatternBass[0][36] = 50; mPatternBass[0][38] = 62; + mPatternBass[0][40] = 45; mPatternBass[0][42] = 57; mPatternBass[0][44] = 45; mPatternBass[0][46] = 57; + mPatternBass[0][48] = 40; mPatternBass[0][50] = 52; mPatternBass[0][52] = 40; mPatternBass[0][54] = 52; + mPatternBass[0][56] = 45; mPatternBass[0][58] = 57; mPatternBass[0][60] = 45; mPatternBass[0][62] = 45; + + mPatternChords[0][0] = ChordType::AM; mPatternChords[0][1] = ChordType::AM; + mPatternChords[0][2] = ChordType::E7; mPatternChords[0][3] = ChordType::AM; + mPatternChords[0][4] = ChordType::DM; mPatternChords[0][5] = ChordType::AM; + mPatternChords[0][6] = ChordType::E7; mPatternChords[0][7] = ChordType::AM; + + // PATTERN 1: Fast octave higher walking bass + for(int i=0; i<64; ++i) { + if(mPatternLead[0][i] > 0) mPatternLead[1][i] = mPatternLead[0][i] + 12; + } + mPatternLead[1][32] = 86; mPatternLead[1][33] = 0; mPatternLead[1][34] = 86; mPatternLead[1][35] = 89; + mPatternLead[1][36] = 93; mPatternLead[1][37] = 0; mPatternLead[1][38] = 91; mPatternLead[1][39] = 89; + mPatternLead[1][40] = 88; mPatternLead[1][41] = 0; mPatternLead[1][42] = 84; mPatternLead[1][43] = 84; + mPatternLead[1][44] = 88; mPatternLead[1][45] = 0; mPatternLead[1][46] = 86; mPatternLead[1][47] = 84; + + mPatternBass[1][0] = 45; mPatternBass[1][2] = 57; mPatternBass[1][4] = 60; mPatternBass[1][6] = 57; + mPatternBass[1][8] = 45; mPatternBass[1][10] = 57; mPatternBass[1][12] = 60; mPatternBass[1][14] = 57; + mPatternBass[1][16] = 40; mPatternBass[1][18] = 52; mPatternBass[1][20] = 56; mPatternBass[1][22] = 52; + mPatternBass[1][24] = 45; mPatternBass[1][26] = 57; mPatternBass[1][28] = 45; mPatternBass[1][30] = 45; + mPatternBass[1][32] = 50; mPatternBass[1][34] = 62; mPatternBass[1][36] = 65; mPatternBass[1][38] = 62; + mPatternBass[1][40] = 45; mPatternBass[1][42] = 57; mPatternBass[1][44] = 60; mPatternBass[1][46] = 57; + mPatternBass[1][48] = 40; mPatternBass[1][50] = 52; mPatternBass[1][52] = 56; mPatternBass[1][54] = 52; + mPatternBass[1][56] = 45; mPatternBass[1][58] = 57; mPatternBass[1][60] = 45; mPatternBass[1][62] = 45; + + for(int i=0; i<8; ++i) mPatternChords[1][i] = mPatternChords[0][i]; + + // PATTERN 2: Bridge Motif + mPatternLead[2][0] = 81; mPatternLead[2][2] = 84; mPatternLead[2][4] = 89; mPatternLead[2][6] = 84; + mPatternBass[2][0] = 41; mPatternBass[2][2] = 53; mPatternBass[2][4] = 41; mPatternBass[2][6] = 53; + + mPatternLead[2][8] = 79; mPatternLead[2][10] = 83; mPatternLead[2][12] = 86; mPatternLead[2][14] = 83; + mPatternBass[2][8] = 43; mPatternBass[2][10] = 55; mPatternBass[2][12] = 43; mPatternBass[2][14] = 55; + + mPatternLead[2][16] = 84; mPatternLead[2][18] = 88; mPatternLead[2][20] = 91; mPatternLead[2][22] = 88; + mPatternBass[2][16] = 48; mPatternBass[2][18] = 60; mPatternBass[2][20] = 48; mPatternBass[2][22] = 60; + + mPatternLead[2][24] = 81; mPatternLead[2][26] = 84; mPatternLead[2][28] = 88; mPatternLead[2][30] = 84; + mPatternBass[2][24] = 45; mPatternBass[2][26] = 57; mPatternBass[2][28] = 45; mPatternBass[2][30] = 57; + + mPatternLead[2][32] = 89; mPatternLead[2][34] = 86; mPatternLead[2][36] = 81; mPatternLead[2][38] = 86; + mPatternBass[2][32] = 50; mPatternBass[2][34] = 62; mPatternBass[2][36] = 50; mPatternBass[2][38] = 62; + + mPatternLead[2][40] = 88; mPatternLead[2][42] = 84; mPatternLead[2][44] = 81; mPatternLead[2][46] = 84; + mPatternBass[2][40] = 45; mPatternBass[2][42] = 57; mPatternBass[2][44] = 45; mPatternBass[2][46] = 57; + + mPatternLead[2][48] = 88; mPatternLead[2][50] = 86; mPatternLead[2][52] = 83; mPatternLead[2][54] = 80; + mPatternBass[2][48] = 40; mPatternBass[2][50] = 52; mPatternBass[2][52] = 40; mPatternBass[2][54] = 52; + + mPatternLead[2][56] = 81; mPatternLead[2][57] = 81; mPatternLead[2][58] = 81; + mPatternBass[2][56] = 45; mPatternBass[2][58] = 45; mPatternBass[2][60] = 45; + + mPatternChords[2][0] = ChordType::F; mPatternChords[2][1] = ChordType::G; + mPatternChords[2][2] = ChordType::C; mPatternChords[2][3] = ChordType::AM; + mPatternChords[2][4] = ChordType::DM; mPatternChords[2][5] = ChordType::AM; + mPatternChords[2][6] = ChordType::E7; mPatternChords[2][7] = ChordType::AM; + + // PATTERN 3: Driving Resolution + mPatternLead[3][0] = 81; mPatternLead[3][1] = 79; mPatternLead[3][2] = 77; mPatternLead[3][3] = 81; + mPatternLead[3][4] = 84; mPatternLead[3][5] = 81; mPatternLead[3][6] = 77; mPatternLead[3][7] = 81; + mPatternBass[3][0] = 41; mPatternBass[3][2] = 53; mPatternBass[3][4] = 57; mPatternBass[3][6] = 53; + + mPatternLead[3][8] = 83; mPatternLead[3][9] = 81; mPatternLead[3][10] = 79; mPatternLead[3][11] = 83; + mPatternLead[3][12] = 86; mPatternLead[3][13] = 83; mPatternLead[3][14] = 79; mPatternLead[3][15] = 83; + mPatternBass[3][8] = 43; mPatternBass[3][10] = 55; mPatternBass[3][12] = 59; mPatternBass[3][14] = 55; + + mPatternLead[3][16] = 84; mPatternLead[3][17] = 83; mPatternLead[3][18] = 84; mPatternLead[3][19] = 86; + mPatternLead[3][20] = 88; mPatternLead[3][21] = 86; mPatternLead[3][22] = 84; mPatternLead[3][23] = 88; + mPatternBass[3][16] = 48; mPatternBass[3][18] = 60; mPatternBass[3][20] = 64; mPatternBass[3][22] = 60; + + mPatternLead[3][24] = 81; mPatternLead[3][25] = 80; mPatternLead[3][26] = 81; mPatternLead[3][27] = 83; + mPatternLead[3][28] = 84; mPatternLead[3][29] = 83; mPatternLead[3][30] = 81; mPatternLead[3][31] = 84; + mPatternBass[3][24] = 45; mPatternBass[3][26] = 57; mPatternBass[3][28] = 60; mPatternBass[3][30] = 57; + + mPatternLead[3][32] = 86; mPatternLead[3][33] = 84; mPatternLead[3][34] = 86; mPatternLead[3][35] = 88; + mPatternLead[3][36] = 89; mPatternLead[3][37] = 88; mPatternLead[3][38] = 86; mPatternLead[3][39] = 89; + mPatternBass[3][32] = 50; mPatternBass[3][34] = 62; mPatternBass[3][36] = 65; mPatternBass[3][38] = 62; + + mPatternLead[3][40] = 88; mPatternLead[3][41] = 86; mPatternLead[3][42] = 84; mPatternLead[3][43] = 83; + mPatternLead[3][44] = 81; mPatternLead[3][45] = 84; mPatternLead[3][46] = 88; mPatternLead[3][47] = 84; + mPatternBass[3][40] = 45; mPatternBass[3][42] = 57; mPatternBass[3][44] = 60; mPatternBass[3][46] = 57; + + mPatternLead[3][48] = 83; mPatternLead[3][49] = 81; mPatternLead[3][50] = 80; mPatternLead[3][51] = 81; + mPatternLead[3][52] = 83; mPatternLead[3][53] = 84; mPatternLead[3][54] = 86; mPatternLead[3][55] = 83; + mPatternBass[3][48] = 40; mPatternBass[3][50] = 52; mPatternBass[3][52] = 56; mPatternBass[3][54] = 52; + + mPatternLead[3][56] = 81; mPatternLead[3][58] = 81; mPatternLead[3][60] = 69; + mPatternBass[3][56] = 45; mPatternBass[3][58] = 45; mPatternBass[3][60] = 45; + + for(int i=0; i<8; ++i) mPatternChords[3][i] = mPatternChords[2][i]; + + // PATTERN 4: Slow menacing / half-time feel of original + for(int i=0; i<8; ++i) mPatternChords[4][i] = mPatternChords[0][i]; + mPatternLead[4][0] = 64; mPatternLead[4][4] = 59; mPatternLead[4][8] = 60; mPatternLead[4][12] = 62; + mPatternLead[4][16] = 59; mPatternLead[4][20] = 60; mPatternLead[4][24] = 57; mPatternLead[4][28] = 57; + mPatternLead[4][32] = 62; mPatternLead[4][36] = 65; mPatternLead[4][40] = 64; mPatternLead[4][44] = 60; + mPatternLead[4][48] = 59; mPatternLead[4][52] = 60; mPatternLead[4][56] = 57; mPatternLead[4][60] = 57; + + mPatternBass[4][0] = 33; mPatternBass[4][8] = 33; + mPatternBass[4][16] = 28; mPatternBass[4][24] = 33; + mPatternBass[4][32] = 38; mPatternBass[4][40] = 33; + mPatternBass[4][48] = 28; mPatternBass[4][56] = 33; + + // PATTERN 5: Frantic Arpeggiator (uses Bridge chords) + for(int i=0; i<8; ++i) mPatternChords[5][i] = mPatternChords[2][i]; + for(int b=0; b<8; ++b) { + int root = 0; + switch(mPatternChords[5][b]) { + case ChordType::F: root = 77; break; + case ChordType::G: root = 79; break; + case ChordType::C: root = 84; break; + case ChordType::AM: root = 81; break; + case ChordType::DM: root = 86; break; + case ChordType::E7: root = 80; break; + } + for(int t=0; t<8; ++t) { + if(t%4 == 0) mPatternLead[5][b*8+t] = root; + else if(t%4 == 1) mPatternLead[5][b*8+t] = root+4; + else if(t%4 == 2) mPatternLead[5][b*8+t] = root+7; + else if(t%4 == 3) mPatternLead[5][b*8+t] = root+12; + } + if(mPatternChords[5][b] == ChordType::AM || mPatternChords[5][b] == ChordType::DM) { + mPatternLead[5][b*8+1] -= 1; + mPatternLead[5][b*8+5] -= 1; + } + mPatternBass[5][b*8] = root - 36; + mPatternBass[5][b*8+2] = root - 24; + mPatternBass[5][b*8+4] = root - 36; + mPatternBass[5][b*8+6] = root - 24; + } + + shufflePlaylist(); +} + +void Synth::triggerMusicNote(int voiceIdx, uint8_t midiNote, WaveType wave, float duration, float pan, float volume) { + if (midiNote == 0) { + if (mVoices[voiceIdx].active && !mVoices[voiceIdx].releasing) { + mVoices[voiceIdx].releasing = true; + mVoices[voiceIdx].releaseAge = 0.0f; + } + return; + } + + Voice& v = mVoices[voiceIdx]; + v.active = true; + v.wave = wave; + v.frequency = midiToFreq(midiNote); + v.phase = 0.0f; + v.volume = volume; + v.pan = pan; + v.releasing = false; + v.age = 0.0f; + v.releaseAge = 0.0f; + v.sweepDuration = 0.0f; + + // Default music envelopes + if (voiceIdx == 0) { // Lead + v.adsr.attackTime = 0.005f; + v.adsr.decayTime = 0.03f; + v.adsr.sustainLevel = 0.6f; + v.adsr.releaseTime = duration * 0.4f; + + // Add a soft vibrato for the lead melody + v.vibratoFreq = 6.0f; // 6 Hz LFO + v.vibratoDepth = 0.008f; // ~0.8% frequency wobble + } else if (voiceIdx == 1) { // Harmony + v.adsr.attackTime = 0.01f; + v.adsr.decayTime = 0.05f; + v.adsr.sustainLevel = 0.5f; + v.adsr.releaseTime = duration * 0.3f; + v.vibratoDepth = 0.0f; + } else if (voiceIdx == 2) { // Bass + v.adsr.attackTime = 0.005f; + v.adsr.decayTime = 0.04f; + v.adsr.sustainLevel = 0.7f; + v.adsr.releaseTime = duration * 0.2f; + v.vibratoDepth = 0.0f; + } +} + +void Synth::updateSequencer(float dt) { + if (!mBGMEnabled) return; + + mTickTimer += dt; + if (mTickTimer >= mSecsPerTick) { + mTickTimer -= mSecsPerTick; + + // Advance sequencer tick + mCurrentTick = (mCurrentTick + 1) % 256; + if (mCurrentTick == 0) { + shufflePlaylist(); + } + + int patternIdx = mPlaylist[mCurrentTick / 64]; + int stepInPattern = mCurrentTick % 64; + + // 1. Trigger Lead Note (Voice 0) + uint8_t leadNote = mPatternLead[patternIdx][stepInPattern]; + if (leadNote > 0) { + triggerMusicNote(0, leadNote, WaveType::SQUARE, mSecsPerTick, 0.35f, 0.12f); + } else { + if (mVoices[0].active && !mVoices[0].releasing) { + mVoices[0].releasing = true; + mVoices[0].releaseAge = 0.0f; + } + } + + // 2. Trigger Bass Note (Voice 2) + uint8_t bassNote = mPatternBass[patternIdx][stepInPattern]; + if (bassNote > 0) { + triggerMusicNote(2, bassNote, WaveType::TRIANGLE, mSecsPerTick, 0.5f, 0.22f); + } else { + if (mVoices[2].active && !mVoices[2].releasing) { + mVoices[2].releasing = true; + mVoices[2].releaseAge = 0.0f; + } + } + + // 3. Trigger Harmony/Chord Arpeggio (Voice 1) + int subTick = mCurrentTick % 4; // arpeggiate at 8th note speed + uint8_t harmonyNote = 0; + + ChordType currentChord = mPatternChords[patternIdx][stepInPattern / 8]; + + if (currentChord == ChordType::F) { + uint8_t arpeggio[] = {53, 57, 60, 57}; // F3, A3, C4, A3 + harmonyNote = arpeggio[subTick]; + } else if (currentChord == ChordType::G) { + uint8_t arpeggio[] = {55, 59, 62, 59}; // G3, B3, D4, B3 + harmonyNote = arpeggio[subTick]; + } else if (currentChord == ChordType::C) { + uint8_t arpeggio[] = {48, 55, 60, 55}; // C3, G3, C4, G3 + harmonyNote = arpeggio[subTick]; + } else if (currentChord == ChordType::DM) { + uint8_t arpeggio[] = {57, 62, 65, 62}; // A3, D4, F4, D4 + harmonyNote = arpeggio[subTick]; + } else if (currentChord == ChordType::E7) { + uint8_t arpeggio[] = {56, 59, 64, 59}; // G#3, B3, E4, B3 + harmonyNote = arpeggio[subTick]; + } else { // AM + uint8_t arpeggio[] = {57, 60, 64, 60}; // A3, C4, E4, C4 + harmonyNote = arpeggio[subTick]; + } + + triggerMusicNote(1, harmonyNote, WaveType::TRIANGLE, mSecsPerTick * 0.9f, 0.65f, 0.08f); + + // 4. Trigger Procedural Retro Drums (Voice 3) - Kick, Hat, Snare synthesis! + int drumTick = mCurrentTick % 8; + if (drumTick == 0) { // Kick drum: fast triangle frequency sweep (A1 55Hz to A0 27Hz) + Voice& v = mVoices[3]; + v.active = true; + v.wave = WaveType::TRIANGLE; + v.frequency = 120.0f; + v.startFreq = 120.0f; + v.targetFreq = 30.0f; + v.sweepDuration = 0.09f; + v.sweepAge = 0.0f; + v.phase = 0.0f; + v.volume = 0.45f; + v.pan = 0.5f; + v.releasing = false; + v.age = 0.0f; + v.releaseAge = 0.0f; + v.vibratoDepth = 0.0f; + v.adsr.attackTime = 0.002f; + v.adsr.decayTime = 0.08f; + v.adsr.sustainLevel = 0.1f; + v.adsr.releaseTime = 0.05f; + } else if (drumTick == 4) { // Snare drum: White noise burst + Triangle pop + Voice& v = mVoices[3]; + v.active = true; + v.wave = WaveType::NOISE; + v.frequency = 100.0f; // dummy frequency + v.phase = 0.0f; + v.volume = 0.26f; + v.pan = 0.5f; + v.releasing = false; + v.age = 0.0f; + v.releaseAge = 0.0f; + v.vibratoDepth = 0.0f; + v.sweepDuration = 0.0f; + v.adsr.attackTime = 0.001f; + v.adsr.decayTime = 0.12f; + v.adsr.sustainLevel = 0.05f; + v.adsr.releaseTime = 0.04f; + } else if (drumTick == 2 || drumTick == 6 || drumTick == 7) { // Hi-Hat: very fast noise pop + Voice& v = mVoices[3]; + v.active = true; + v.wave = WaveType::NOISE; + v.frequency = 100.0f; // dummy + v.phase = 0.0f; + v.volume = 0.09f; + v.pan = 0.55f; + v.releasing = false; + v.age = 0.0f; + v.releaseAge = 0.0f; + v.vibratoDepth = 0.0f; + v.sweepDuration = 0.0f; + v.adsr.attackTime = 0.001f; + v.adsr.decayTime = 0.025f; + v.adsr.sustainLevel = 0.0f; + v.adsr.releaseTime = 0.015f; + } + } +} + +void Synth::triggerSFX(SFXType type) { + std::lock_guard lock(mMutex); + + // Choose one of the dedicated SFX voices (Voices 4, 5, 6, 7) based on sound type + // to prevent overlap truncation. + int voiceIdx = 4; + Voice* v = &mVoices[voiceIdx]; + + switch (type) { + case SFXType::MOVE: // Voice 4: quick, quiet triangle pluck + v->active = true; + v->wave = WaveType::TRIANGLE; + v->frequency = 180.0f; + v->phase = 0.0f; + v->volume = 0.22f; + v->pan = 0.45f; + v->releasing = false; + v->age = 0.0f; + v->releaseAge = 0.0f; + v->sweepDuration = 0.0f; + v->vibratoDepth = 0.0f; + v->adsr.attackTime = 0.001f; + v->adsr.decayTime = 0.04f; + v->adsr.sustainLevel = 0.0f; + v->adsr.releaseTime = 0.02f; + break; + + case SFXType::ROTATE: // Voice 5: quick laser frequency sweep + voiceIdx = 5; + v = &mVoices[voiceIdx]; + v->active = true; + v->wave = WaveType::SQUARE; + v->frequency = 330.0f; + v->startFreq = 330.0f; + v->targetFreq = 660.0f; + v->sweepDuration = 0.08f; + v->sweepAge = 0.0f; + v->phase = 0.0f; + v->volume = 0.07f; + v->pan = 0.55f; + v->releasing = false; + v->age = 0.0f; + v->releaseAge = 0.0f; + v->vibratoDepth = 0.0f; + v->adsr.attackTime = 0.002f; + v->adsr.decayTime = 0.06f; + v->adsr.sustainLevel = 0.0f; + v->adsr.releaseTime = 0.03f; + break; + + case SFXType::LAND: // Voice 4: sub-bass thud (sine decay) + v->active = true; + v->wave = WaveType::SINE; + v->frequency = 90.0f; + v->startFreq = 90.0f; + v->targetFreq = 45.0f; + v->sweepDuration = 0.08f; + v->sweepAge = 0.0f; + v->phase = 0.0f; + v->volume = 0.35f; + v->pan = 0.5f; + v->releasing = false; + v->age = 0.0f; + v->releaseAge = 0.0f; + v->vibratoDepth = 0.0f; + v->adsr.attackTime = 0.003f; + v->adsr.decayTime = 0.10f; + v->adsr.sustainLevel = 0.0f; + v->adsr.releaseTime = 0.04f; + break; + + case SFXType::LINE_CLEAR: // Voice 6: rapid, bright ascending arpeggio (C5-E5-G5-C6) + voiceIdx = 6; + v = &mVoices[voiceIdx]; + v->active = true; + v->wave = WaveType::SQUARE; + v->frequency = midiToFreq(72); // C5 + v->startFreq = midiToFreq(72); + v->targetFreq = midiToFreq(84); // sweeps to C6 + v->sweepDuration = 0.22f; + v->sweepAge = 0.0f; + v->phase = 0.0f; + v->volume = 0.11f; + v->pan = 0.5f; + v->releasing = false; + v->age = 0.0f; + v->releaseAge = 0.0f; + v->vibratoDepth = 0.0f; + v->adsr.attackTime = 0.005f; + v->adsr.decayTime = 0.18f; + v->adsr.sustainLevel = 0.0f; + v->adsr.releaseTime = 0.10f; + break; + + case SFXType::TETRIS_CLEAR: // Voice 6: dramatic multi-freq sweep + noise chord + voiceIdx = 6; + v = &mVoices[voiceIdx]; + v->active = true; + v->wave = WaveType::SQUARE; + v->frequency = midiToFreq(72); // C5 + v->startFreq = midiToFreq(72); + v->targetFreq = midiToFreq(96); // sweeps high! C7 + v->sweepDuration = 0.40f; + v->sweepAge = 0.0f; + v->phase = 0.0f; + v->volume = 0.13f; + v->pan = 0.5f; + v->releasing = false; + v->age = 0.0f; + v->releaseAge = 0.0f; + v->vibratoDepth = 0.015f; // extreme vibrato! + v->vibratoFreq = 12.0f; + v->adsr.attackTime = 0.01f; + v->adsr.decayTime = 0.35f; + v->adsr.sustainLevel = 0.0f; + v->adsr.releaseTime = 0.20f; + + // Trigger extra white noise burst on SFX voice 7 for explosive impact + v = &mVoices[7]; + v->active = true; + v->wave = WaveType::NOISE; + v->phase = 0.0f; + v->volume = 0.22f; + v->pan = 0.5f; + v->releasing = false; + v->age = 0.0f; + v->releaseAge = 0.0f; + v->sweepDuration = 0.0f; + v->vibratoDepth = 0.0f; + v->adsr.attackTime = 0.005f; + v->adsr.decayTime = 0.30f; + v->adsr.sustainLevel = 0.0f; + v->adsr.releaseTime = 0.15f; + break; + + case SFXType::LEVEL_UP: // Voice 6: triumphant two-note chord fanfares + voiceIdx = 6; + v = &mVoices[voiceIdx]; + v->active = true; + v->wave = WaveType::SQUARE; + v->frequency = midiToFreq(76); // E5 + v->startFreq = midiToFreq(76); + v->targetFreq = midiToFreq(88); // Sweeps to E6 + v->sweepDuration = 0.30f; + v->sweepAge = 0.0f; + v->phase = 0.0f; + v->volume = 0.11f; + v->pan = 0.40f; + v->releasing = false; + v->age = 0.0f; + v->releaseAge = 0.0f; + v->vibratoDepth = 0.0f; + v->adsr.attackTime = 0.005f; + v->adsr.decayTime = 0.20f; + v->adsr.sustainLevel = 0.0f; + v->adsr.releaseTime = 0.15f; + + // Trigger third note harmony on voice 7 + v = &mVoices[7]; + v->active = true; + v->wave = WaveType::SQUARE; + v->frequency = midiToFreq(80); // G#5 + v->startFreq = midiToFreq(80); + v->targetFreq = midiToFreq(92); // Sweeps to G#6 + v->sweepDuration = 0.30f; + v->sweepAge = 0.0f; + v->phase = 0.0f; + v->volume = 0.10f; + v->pan = 0.60f; + v->releasing = false; + v->age = 0.0f; + v->releaseAge = 0.0f; + v->vibratoDepth = 0.0f; + v->adsr.attackTime = 0.005f; + v->adsr.decayTime = 0.20f; + v->adsr.sustainLevel = 0.0f; + v->adsr.releaseTime = 0.15f; + break; + + case SFXType::GAME_OVER: // Voice 6: sad, sliding down detuned chord + voiceIdx = 6; + v = &mVoices[voiceIdx]; + v->active = true; + v->wave = WaveType::TRIANGLE; + v->frequency = 220.0f; + v->startFreq = 220.0f; + v->targetFreq = 80.0f; // sweep down + v->sweepDuration = 0.80f; + v->sweepAge = 0.0f; + v->phase = 0.0f; + v->volume = 0.35f; + v->pan = 0.40f; + v->releasing = false; + v->age = 0.0f; + v->releaseAge = 0.0f; + v->vibratoDepth = 0.02f; // highly detuned detuning wobble + v->vibratoFreq = 8.0f; + v->adsr.attackTime = 0.01f; + v->adsr.decayTime = 0.60f; + v->adsr.sustainLevel = 0.2f; // Game Over chord detunes and sustains nicely! + v->adsr.releaseTime = 0.40f; + + // Voice 7 plays detuned counter note + v = &mVoices[7]; + v->active = true; + v->wave = WaveType::SQUARE; + v->frequency = 233.0f; // slightly detuned semitone above + v->startFreq = 233.0f; + v->targetFreq = 83.0f; // sweep down + v->sweepDuration = 0.80f; + v->sweepAge = 0.0f; + v->phase = 0.0f; + v->volume = 0.08f; + v->pan = 0.60f; + v->releasing = false; + v->age = 0.0f; + v->releaseAge = 0.0f; + v->vibratoDepth = 0.0f; + v->adsr.attackTime = 0.01f; + v->adsr.decayTime = 0.60f; + v->adsr.sustainLevel = 0.2f; + v->adsr.releaseTime = 0.40f; + break; + } +} + +void Synth::updateVoice(Voice& voice, float dt) { + if (!voice.active) return; + + if (voice.releasing) { + voice.releaseAge += dt; + if (voice.releaseAge >= voice.adsr.releaseTime) { + voice.active = false; + return; + } + } else { + voice.age += dt; + if (voice.adsr.sustainLevel == 0.0f && voice.age >= voice.adsr.attackTime + voice.adsr.decayTime) { + voice.active = false; + return; + } + } + + // Apply frequency sweeps if active + if (voice.sweepDuration > 0.0f) { + voice.sweepAge += dt; + if (voice.sweepAge >= voice.sweepDuration) { + voice.frequency = voice.targetFreq; + voice.sweepDuration = 0.0f; + } else { + float t = voice.sweepAge / voice.sweepDuration; + voice.frequency = voice.startFreq + (voice.targetFreq - voice.startFreq) * t; + } + } +} + +float Synth::generateSample(Voice& voice) { + if (!voice.active) return 0.0f; + + // Calculate ADSR envelope volume multiplier + float envVolume = 0.0f; + if (voice.releasing) { + if (voice.releaseAge < voice.adsr.releaseTime) { + float progress = voice.releaseAge / voice.adsr.releaseTime; + envVolume = voice.adsr.sustainLevel * (1.0f - progress); + } + } else { + float age = voice.age; + if (age < voice.adsr.attackTime) { + envVolume = age / voice.adsr.attackTime; + } else if (age < voice.adsr.attackTime + voice.adsr.decayTime) { + float progress = (age - voice.adsr.attackTime) / voice.adsr.decayTime; + envVolume = 1.0f - (1.0f - voice.adsr.sustainLevel) * progress; + } else { + envVolume = voice.adsr.sustainLevel; + } + } + + if (envVolume <= 0.0f) { + return 0.0f; + } + + // Apply vibrato modulation + float modulatedFreq = voice.frequency; + if (voice.vibratoDepth > 0.0f) { + float lfo = std::sin(voice.age * 2.0f * M_PI * voice.vibratoFreq); + modulatedFreq += lfo * voice.vibratoDepth * voice.frequency; + } + + // Phase increment + float phaseInc = 2.0f * M_PI * modulatedFreq / 44100.0f; + voice.phase += phaseInc; + if (voice.phase >= 2.0f * M_PI) { + voice.phase -= 2.0f * M_PI; + } + + // Oscillator waveform generation + float oscValue = 0.0f; + switch (voice.wave) { + case WaveType::SINE: + oscValue = std::sin(voice.phase); + break; + case WaveType::SQUARE: + // Custom pulse width modulation (50% standard duty cycle, slightly narrow) + oscValue = (std::sin(voice.phase) >= 0.1f ? 1.0f : -1.0f); + break; + case WaveType::TRIANGLE: { + float normalizedPhase = voice.phase / (2.0f * M_PI); + oscValue = 4.0f * std::abs(normalizedPhase - std::floor(normalizedPhase + 0.5f)) - 1.0f; + break; + } + case WaveType::NOISE: + oscValue = (((float)std::rand() / RAND_MAX) * 2.0f - 1.0f); + break; + } + + return oscValue * voice.volume * envVolume; +} + +void Synth::audioCallback(Uint8* stream, int len) { + int frames = len / 4; // 16-bit signed stereo = 4 bytes per frame + int16_t* out = (int16_t*)stream; + + float dt = 1.0f / 44100.0f; + + std::lock_guard lock(mMutex); + + for (int f = 0; f < frames; ++f) { + // Advance chiptune sequencer clock + updateSequencer(dt); + + float leftMixed = 0.0f; + float rightMixed = 0.0f; + + // Render each voice and mix with panning + for (int i = 0; i < NUM_VOICES; ++i) { + if (!mVoices[i].active) continue; + + // Render and update phase/envelope + float voiceSample = generateSample(mVoices[i]); + updateVoice(mVoices[i], dt); + + // Stereo panning + leftMixed += voiceSample * (1.0f - mVoices[i].pan); + rightMixed += voiceSample * mVoices[i].pan; + } + + // Clamp mixed signals to prevent audio clipping + leftMixed = std::clamp(leftMixed, -1.0f, 1.0f); + rightMixed = std::clamp(rightMixed, -1.0f, 1.0f); + + // Convert mixed floats back to 16-bit PCM samples + out[f * 2] = (int16_t)(leftMixed * 32767.0f); + out[f * 2 + 1] = (int16_t)(rightMixed * 32767.0f); + + // Log sample into the visualizer ring buffer + float avgSample = (leftMixed + rightMixed) * 0.5f; + mVisBuffer[mVisWritePos] = avgSample; + mVisWritePos = (mVisWritePos + 1) % VIS_BUFFER_SIZE; + } +} + +void Synth::audioCallbackWrapper(void* userdata, Uint8* stream, int len) { + ((Synth*)userdata)->audioCallback(stream, len); +} + +std::vector Synth::getVisualizerBuffer() { + std::lock_guard lock(mMutex); + std::vector res(VIS_BUFFER_SIZE); + + // Copy the ring buffer chronologically starting from the current write position + for (int i = 0; i < VIS_BUFFER_SIZE; ++i) { + int idx = (mVisWritePos + i) % VIS_BUFFER_SIZE; + res[i] = mVisBuffer[idx]; + } + return res; +} diff --git a/Synth.hpp b/Synth.hpp new file mode 100644 index 0000000..4fbe49f --- /dev/null +++ b/Synth.hpp @@ -0,0 +1,125 @@ +#pragma once +#include +#include +#include + +enum class WaveType { + SINE, + SQUARE, + TRIANGLE, + NOISE +}; + +enum class ChordType { + AM, + E7, + DM, + F, + G, + C +}; + +struct Envelope { + float attackTime = 0.005f; // seconds + float decayTime = 0.05f; // seconds + float sustainLevel = 0.7f; // 0.0 to 1.0 + float releaseTime = 0.12f; // seconds +}; + +struct Voice { + bool active = false; + WaveType wave = WaveType::SQUARE; + float frequency = 0.0f; + float phase = 0.0f; + float volume = 0.12f; + float pan = 0.5f; // 0.0 (left) to 1.0 (right) + + // Envelope tracking + Envelope adsr; + float age = 0.0f; // duration active (seconds) + float releaseAge = 0.0f; // duration in release phase (seconds) + bool releasing = false; + + // Frequency sweeps (for sound effects) + float startFreq = 0.0f; + float targetFreq = 0.0f; + float sweepDuration = 0.0f; + float sweepAge = 0.0f; + + // Vibrato + float vibratoFreq = 0.0f; // LFO speed (Hz) + float vibratoDepth = 0.0f; // pitch variation depth +}; + +enum class SFXType { + MOVE, + ROTATE, + LAND, + LINE_CLEAR, + TETRIS_CLEAR, + LEVEL_UP, + GAME_OVER +}; + +class Synth { +public: + Synth(); + ~Synth(); + + bool init(); + void playBGM(bool play); + void triggerSFX(SFXType type); + void stopAllSFX(); + + // Thread-safe copy of visualizer buffer for the GPU renderer + std::vector getVisualizerBuffer(); + + // SDL Audio Callback + void audioCallback(Uint8* stream, int len); + +private: + static void audioCallbackWrapper(void* userdata, Uint8* stream, int len); + + SDL_AudioDeviceID mAudioDevice = 0; + bool mBGMEnabled = false; + + std::mutex mMutex; + + // 8 Dedicated voices: + // Voice 0: BGM Lead (Square) + // Voice 1: BGM Harmony / Echo (Pulse) + // Voice 2: BGM Bass (Triangle) + // Voice 3: BGM Drums / Noise (Noise/Triangle) + // Voice 4, 5, 6, 7: Dedicated SFX voices + static constexpr int NUM_VOICES = 8; + Voice mVoices[NUM_VOICES]; + + // Sequencer state + int mBPM = 138; + float mSecsPerTick = 0.0f; // Seconds per eighth note tick + float mTickTimer = 0.0f; + int mCurrentTick = 0; + + // Pattern-based Sequencer + static constexpr int NUM_PATTERNS = 6; + uint8_t mPatternLead[NUM_PATTERNS][64]; + uint8_t mPatternBass[NUM_PATTERNS][64]; + ChordType mPatternChords[NUM_PATTERNS][8]; // 8 bars per pattern + + // Dynamic Playlist (4 patterns per cycle) + int mPlaylist[4]; + void shufflePlaylist(); + + // Visualizer Oscilloscope Ring Buffer + static constexpr int VIS_BUFFER_SIZE = 512; + float mVisBuffer[VIS_BUFFER_SIZE]; + int mVisWritePos = 0; + + void initSequencer(); + void updateSequencer(float dt); + void updateVoice(Voice& voice, float dt); + float generateSample(Voice& voice); + + // Helper to start BGM notes + void triggerMusicNote(int voiceIdx, uint8_t midiNote, WaveType wave, float duration, float pan = 0.5f, float volume = 0.1f); +}; diff --git a/launch_miyoo.sh b/launch_miyoo.sh new file mode 100644 index 0000000..1fa5050 --- /dev/null +++ b/launch_miyoo.sh @@ -0,0 +1,50 @@ +#!/bin/sh +# Launcher CyberMatris per Miyoo Mini Plus (OnionOS / Stock OS) +GAMEDIR=$(dirname "$0") +LOGFILE="$GAMEDIR/cybermatris.log" +exec > "$LOGFILE" 2>&1 +set -x + +echo "=== CyberMatris Launch $(date) ===" + +# Questo è fondamentale: indica a SDL2 di usare il backend video nativo Miyoo +export SDL_VIDEODRIVER=mmiyoo +unset SDL_RENDER_DRIVER +unset LD_PRELOAD + +export HOME=/mnt/SDCARD +export LD_LIBRARY_PATH="$GAMEDIR:/config/lib:/customer/lib:/mnt/SDCARD/.tmp_update/lib/parasyte:/mnt/SDCARD/usr/local/lib:/mnt/SDCARD/usr/lib/arm-linux-gnueabihf" + +# Ferma l'audioserver per liberare il device audio hardware (/dev/mi_ao) +killall -9 audioserver 2>/dev/null || true +sleep 1 + +# Rilevamento dello schermo e impostazione della risoluzione nativa +# Controlliamo la risoluzione corrente PRIMA di modificarla per evitare di forzare +# parametri fuori specifica che congelano il controller dello schermo del Miyoo Mini Plus. +if fbset | grep -q "752"; then + echo "[LAUNCHER] Rilevato schermo Miyoo Mini V4 (752x560)!" + fbset -g 752 560 752 1120 32 2>/dev/null + IS_V4=1 + export MIYOO_SCREEN_WIDTH=752 + export MIYOO_SCREEN_HEIGHT=560 +else + echo "[LAUNCHER] Schermo standard 640x480 (Miyoo Mini Plus / v1/v2/v3)" + fbset -g 640 480 640 960 32 2>/dev/null + IS_V4=0 + export MIYOO_SCREEN_WIDTH=640 + export MIYOO_SCREEN_HEIGHT=480 +fi + +cd "$GAMEDIR" +./cybermatris_miyoo +EXIT=$? + +# Ripristina sempre la modalità standard 640x480 all'uscita per evitare disallineamenti con MainUI +if [ "$IS_V4" -eq 1 ]; then + echo "[LAUNCHER] Ripristino risoluzione standard all'uscita..." + fbset -g 640 480 640 960 32 2>/dev/null +fi + +echo "=== EXIT CODE: $EXIT ===" +exit $EXIT diff --git a/main.cpp b/main.cpp new file mode 100644 index 0000000..82540ec --- /dev/null +++ b/main.cpp @@ -0,0 +1,332 @@ +#include +#include +#include +#ifdef MIYOO_BUILD +#include +#include +#include +#include +#include +#endif +#include "Game.hpp" +#include "Synth.hpp" +#include "Renderer.hpp" + +int main([[maybe_unused]] int argc, [[maybe_unused]] char* argv[]) { + // 1. Initialize SDL2 subsystems + if (SDL_Init(SDL_INIT_VIDEO | SDL_INIT_AUDIO) < 0) { + std::cerr << "SDL could not initialize! SDL_Error: " << SDL_GetError() << std::endl; + return -1; + } + + // 2. Create display window + int win_w = 800; + int win_h = 600; + Uint32 win_flags = SDL_WINDOW_SHOWN; + +#ifdef MIYOO_BUILD + win_flags |= SDL_WINDOW_FULLSCREEN; + + // Default fallback + win_w = 640; + win_h = 480; + + // 1. Try environment variables first + const char* env_w = std::getenv("MIYOO_SCREEN_WIDTH"); + const char* env_h = std::getenv("MIYOO_SCREEN_HEIGHT"); + if (env_w && env_h) { + int parsed_w = std::atoi(env_w); + int parsed_h = std::atoi(env_h); + if (parsed_w > 0 && parsed_h > 0) { + win_w = parsed_w; + win_h = parsed_h; + std::cout << "[INFO] Detected screen resolution from environment: " << win_w << "x" << win_h << std::endl; + } + } else { + // 2. Fallback to /dev/fb0 ioctl + int fd = open("/dev/fb0", O_RDONLY); + if (fd >= 0) { + struct fb_var_screeninfo vinfo; + if (ioctl(fd, FBIOGET_VSCREENINFO, &vinfo) >= 0) { + if (vinfo.xres > 0 && vinfo.yres > 0) { + win_w = vinfo.xres; + win_h = vinfo.yres; + std::cout << "[INFO] Detected screen resolution from /dev/fb0: " << win_w << "x" << win_h << std::endl; + } + } + close(fd); + } else { + std::cerr << "[WARNING] Failed to open /dev/fb0 for resolution query, using default 640x480" << std::endl; + } + } +#endif + + SDL_Window* window = SDL_CreateWindow( + "CyberMatris - Programmatic Retro Tetris", + SDL_WINDOWPOS_CENTERED, + SDL_WINDOWPOS_CENTERED, + win_w, win_h, + win_flags + ); + if (window == nullptr) { + std::cerr << "Window could not be created! SDL_Error: " << SDL_GetError() << std::endl; + SDL_Quit(); + return -1; + } + + // 3. Create GPU Hardware Accelerated Renderer with VSync + SDL_Renderer* renderer = SDL_CreateRenderer( + window, -1, + SDL_RENDERER_ACCELERATED | SDL_RENDERER_PRESENTVSYNC + ); +#ifdef MIYOO_BUILD + if (renderer != nullptr) { + std::cerr << "[MIYOO] Hardware accelerated renderer OK" << std::endl; + SDL_RenderSetLogicalSize(renderer, 800, 600); + } +#endif + if (renderer == nullptr) { + std::cerr << "Renderer could not be created! SDL_Error: " << SDL_GetError() << std::endl; + SDL_DestroyWindow(window); + SDL_Quit(); + return -1; + } + + // 4. Initialize Procedural Audio Synthesizer + Synth synth; + if (!synth.init()) { + std::cerr << "Warning: Could not initialize programmatic audio synthesizer! SDL_Error: " << SDL_GetError() << std::endl; + } + + // 5. Initialize Game Logic and Renderer + Game game; + Renderer gameRenderer; + if (!gameRenderer.init(renderer, win_w, win_h)) { + std::cerr << "Failed to build procedural graphics textures!" << std::endl; + SDL_DestroyRenderer(renderer); + SDL_DestroyWindow(window); + SDL_Quit(); + return -1; + } + + bool quit = false; + SDL_Event event; + + // Track frame timing using high-precision performance counters + uint64_t lastTime = SDL_GetPerformanceCounter(); + double freq = static_cast(SDL_GetPerformanceFrequency()); + + float softDropTimer = 0.0f; + + // Flush initial queued events (e.g., from OnionOS menu launch/button presses) + std::cout << "[INFO] Flushing initial event queue..." << std::endl; + SDL_Event trashEvent; + int flushedCount = 0; + while (SDL_PollEvent(&trashEvent)) { + flushedCount++; + } + std::cout << "[INFO] Flushed " << flushedCount << " initial events." << std::endl; + + // 6. Execution Loop + while (!quit) { + // A. Calculate frame delta-time (dt) + uint64_t currentTime = SDL_GetPerformanceCounter(); + float dt = static_cast((currentTime - lastTime) / freq); + lastTime = currentTime; + + // Cap dt to prevent massive jumps during window drags or lags + if (dt > 0.1f) dt = 0.1f; + + // B. Handle Inputs & Events + while (SDL_PollEvent(&event) != 0) { + if (event.type == SDL_QUIT) { + std::cout << "[EVENT] Received SDL_QUIT signal." << std::endl; + quit = true; + } else if (event.type == SDL_KEYDOWN) { + SDL_Keycode sym = event.key.keysym.sym; + std::cout << "[EVENT] Key Down: " << sym << " (" << SDL_GetKeyName(sym) << ")" << std::endl; + + if (sym == SDLK_s) { + gameRenderer.takeScreenshot("screenshot.bmp"); + std::cout << "[INFO] Screenshot captured to screenshot.bmp" << std::endl; + } + + // START Screen Inputs + if (game.getState() == GameState::START) { + if (sym == SDLK_RETURN || sym == SDLK_KP_ENTER || sym == SDLK_LALT) { + std::cout << "[INPUT] Start/Confirm game triggered." << std::endl; + game.setState(GameState::PLAYING); + game.reset(); + synth.playBGM(true); + } else if (sym == SDLK_ESCAPE) { + // Cooldown check (1000ms) to prevent immediate exit due to leftover events on boot + Uint32 currentTicks = SDL_GetTicks(); + if (currentTicks > 1000) { + std::cout << "[INPUT] Quit game requested via ESCAPE/SELECT (ticks: " << currentTicks << ")" << std::endl; + quit = true; + } else { + std::cout << "[INPUT] Blocked early ESCAPE/SELECT quit request (cooldown ticks: " << currentTicks << ")" << std::endl; + } + } + } + // PLAYING State Inputs + else if (game.getState() == GameState::PLAYING) { + switch (sym) { + case SDLK_LEFT: + game.moveLeft(); + break; + case SDLK_RIGHT: + game.moveRight(); + break; + case SDLK_UP: + case SDLK_x: + case SDLK_LALT: // Gamepad A (Rotate CW) + game.rotate(1); // Clockwise rotation + break; + case SDLK_z: + case SDLK_LCTRL: // Gamepad B (Rotate CCW) + game.rotate(-1); // Counter-Clockwise rotation + break; + case SDLK_SPACE: + game.hardDrop(); + break; + case SDLK_c: + case SDLK_LSHIFT: + game.holdPiece(); + break; + case SDLK_p: + game.setState(GameState::PAUSED); + synth.playBGM(false); + break; + case SDLK_r: + synth.stopAllSFX(); + game.reset(); + synth.playBGM(true); + break; + case SDLK_ESCAPE: + synth.stopAllSFX(); + game.setState(GameState::START); + synth.playBGM(false); + break; + default: + break; + } + } + // PAUSED State Inputs + else if (game.getState() == GameState::PAUSED) { + if (sym == SDLK_p) { + game.setState(GameState::PLAYING); + synth.playBGM(true); + } else if (sym == SDLK_ESCAPE) { + synth.stopAllSFX(); + game.setState(GameState::START); + synth.playBGM(false); + } + } + // GAME OVER State Inputs + else if (game.getState() == GameState::GAME_OVER) { + if (sym == SDLK_r) { + synth.stopAllSFX(); + game.setState(GameState::PLAYING); + game.reset(); + synth.playBGM(true); + } else if (sym == SDLK_ESCAPE || sym == SDLK_RETURN || sym == SDLK_KP_ENTER || sym == SDLK_LALT) { + synth.stopAllSFX(); + game.setState(GameState::START); + synth.playBGM(false); + } + } + } + } + + // C. Continuous Keyboard Polling for Smooth Soft-Drop sliding + if (game.getState() == GameState::PLAYING) { + const Uint8* keyboardState = SDL_GetKeyboardState(nullptr); + if (keyboardState[SDL_SCANCODE_DOWN]) { + softDropTimer += dt; + if (softDropTimer >= 0.04f) { // Soft drop tick speed (every 40ms) + softDropTimer = 0.0f; + game.softDrop(); + } + } else { + softDropTimer = 0.0f; + } + } + + // D. Update Game Logic (gravity timing) + bool lineCleared = game.update(dt); + if (lineCleared) { + // Trigger glorious pixel explosion of sparks on cleared rows + gameRenderer.spawnLineClearParticles(game); + } + + // E. Bridge Game Events to Synth Sound Triggers and Particle Emitters + if (game.flagMoveSFX) { + synth.triggerSFX(SFXType::MOVE); + game.flagMoveSFX = false; + } + if (game.flagRotateSFX) { + synth.triggerSFX(SFXType::ROTATE); + game.flagRotateSFX = false; + } + if (game.flagLandSFX) { + synth.triggerSFX(SFXType::LAND); + + // Spawn subtle white-gray impact smoke clouds at bottom row of contact + auto lockedCells = game.mLastLockedCells; + if (!lockedCells.empty()) { + int lowestY = -100; + for (const auto& p : lockedCells) { + if (p.y > lowestY) lowestY = p.y; + } + for (const auto& p : lockedCells) { + if (p.y == lowestY) { + gameRenderer.spawnLandDustParticles(p.x, p.y, game.getActivePieceType()); + } + } + } + game.flagLandSFX = false; + } + if (game.flagLineClearSFX) { + synth.triggerSFX(SFXType::LINE_CLEAR); + game.flagLineClearSFX = false; + } + if (game.flagTetrisClearSFX) { + synth.triggerSFX(SFXType::TETRIS_CLEAR); + game.flagTetrisClearSFX = false; + } + if (game.flagLevelUpSFX) { + synth.triggerSFX(SFXType::LEVEL_UP); + game.flagLevelUpSFX = false; + } + if (game.flagGameOverSFX) { + synth.triggerSFX(SFXType::GAME_OVER); + synth.playBGM(false); + game.flagGameOverSFX = false; + } + + // F. Update Parallax Background and Particle physics + gameRenderer.update(dt); + + // G. Render active frame (automatically uploads backbuffer and calls SDL_RenderPresent) + gameRenderer.render(game, synth); + + // H. Automatic periodic screenshot disabled on Miyoo (FAT32 write-during-render issues) +#ifndef MIYOO_BUILD + static float autoScreenshotTimer = 0.0f; + autoScreenshotTimer += dt; + if (autoScreenshotTimer >= 2.0f) { + autoScreenshotTimer = 0.0f; + gameRenderer.takeScreenshot("screenshot.bmp"); + } +#endif + } + + // 7. Cleanup and close resources + synth.playBGM(false); + SDL_DestroyRenderer(renderer); + SDL_DestroyWindow(window); + SDL_Quit(); + + return 0; +} diff --git a/screenshot.png b/screenshot.png new file mode 100644 index 0000000000000000000000000000000000000000..a3a6f7d611a04fe0afe30eba3d3c04df026bb52f GIT binary patch literal 9477 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 literal 0 HcmV?d00001