commit e0c2cd210a0bc6690f87dad03e0e3bed0214a04f Author: enne2 Date: Thu May 21 15:06:27 2026 +0200 Initial commit of Miyoo Mini Plus port for CyberMatris 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 0000000..a3a6f7d Binary files /dev/null and b/screenshot.png differ