Initial commit of Miyoo Mini Plus port for CyberMatris

This commit is contained in:
2026-05-21 15:06:27 +02:00
commit e0c2cd210a
13 changed files with 3125 additions and 0 deletions
+13
View File
@@ -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
+108
View File
@@ -0,0 +1,108 @@
#pragma once
#include <cstdint>
// 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 ~
};
}
+450
View File
@@ -0,0 +1,450 @@
#include "Game.hpp"
#include <algorithm>
#include <chrono>
#include <iostream>
// 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<PieceType> 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<int>(mActiveType) << std::endl;
mState = GameState::GAME_OVER;
flagGameOverSFX = true;
} else {
std::cout << "[DEBUG] Spawned piece type " << static_cast<int>(mActiveType) << " successfully at (" << mActiveX << ", " << mActiveY << ")" << std::endl;
}
}
std::vector<Point> Game::getActiveCells() const {
std::vector<Point> cells;
if (mActiveType == PieceType::NONE) return cells;
int typeIdx = static_cast<int>(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<Point> Game::getGhostCells() const {
std::vector<Point> 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<Point>& 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<int>(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<Point> 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<Point> 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<Point> 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<Point> 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<Point> 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<Point> 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<int> 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;
}
}
+189
View File
@@ -0,0 +1,189 @@
#pragma once
#include <vector>
#include <random>
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<Point> getActiveCells() const;
// Absolute cell coordinates of where active piece would land (ghost projection)
std::vector<Point> getGhostCells() const;
PieceType getHoldPieceType() const { return mHoldType; }
bool canHold() const { return mCanHold; }
std::vector<PieceType> 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<int> mClearedLinesThisTick;
std::vector<Point> 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<PieceType> mNextQueue;
std::vector<PieceType> 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<Point>& cells, int dx, int dy) const;
bool fits(PieceType type, int rot, int cx, int cy) const;
void lockPiece();
void clearLines();
};
+28
View File
@@ -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
+109
View File
@@ -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"
+845
View File
@@ -0,0 +1,845 @@
#include "Renderer.hpp"
#include "Font.hpp"
#include <cmath>
#include <cstdlib>
#include <sstream>
#include <iomanip>
#include <algorithm>
// 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<uint32_t*>(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<Uint8>(color.r * alpha + dstR * invAlpha);
Uint8 outG = static_cast<Uint8>(color.g * alpha + dstG * invAlpha);
Uint8 outB = static_cast<Uint8>(color.b * alpha + dstB * invAlpha);
Uint8 outA = static_cast<Uint8>(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<uint32_t*>(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<Uint8>(color.r * alpha + dstR * invAlpha);
Uint8 outG = static_cast<Uint8>(color.g * alpha + dstG * invAlpha);
Uint8 outB = static_cast<Uint8>(color.b * alpha + dstB * invAlpha);
Uint8 outA = static_cast<Uint8>(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<float>(std::rand() % 800);
s.y = static_cast<float>(std::rand() % 600);
s.speed = 10.0f + static_cast<float>(std::rand() % 30);
s.size = 1.0f + static_cast<float>(std::rand() % 3);
s.alpha = 50.0f + static_cast<float>(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<float>(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<Uint8>(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<Uint8>(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<Uint8>(std::min(color.r + 60, 255)),
static_cast<Uint8>(std::min(color.g + 60, 255)),
static_cast<Uint8>(std::min(color.b + 60, 255)),
255
};
SDL_Color reflectionCol = {lighterColor.r, lighterColor.g, lighterColor.b, static_cast<Uint8>(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<Uint8>(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<Uint8>(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<Uint8>(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<int>(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<Uint8>(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<int>(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<float>(h - 50));
// Pulsing color cycle: fades from hot purple to high green
SDL_Color barCol = {
static_cast<Uint8>(std::min(rms * 900.0f, 255.0f)),
static_cast<Uint8>(180 - std::min(rms * 300.0f, 180.0f)),
245,
70
};
SDL_Rect barRect = {
dataX + b * (barW + 3),
y + h - 15 - static_cast<int>(barHeight),
barW,
static_cast<int>(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<int>(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<float>(rx);
part.y = static_cast<float>(ry);
// Random explosive velocity vectors
float angle = static_cast<float>(std::rand() % 360) * (3.14159f / 180.0f);
float speed = 50.0f + static_cast<float>(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<float>(std::rand() % 10) * 0.1f; // decay ~0.8s
part.size = 2.0f + static_cast<float>(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<float>(rx + (std::rand() % CELL_SIZE) - CELL_SIZE / 2);
p.y = static_cast<float>(ry - 2);
p.vx = static_cast<float>((std::rand() % 100) - 50); // drift sideways
p.vy = -10.0f - static_cast<float>(std::rand() % 30); // slight upwards float
p.life = 1.0f;
p.decay = 2.0f + static_cast<float>(std::rand() % 10) * 0.2f; // quick decay ~0.4s
p.size = 2.0f + static_cast<float>(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<Uint8>(s.alpha)};
SDL_Rect starRect = {
static_cast<int>(s.x),
static_cast<int>(s.y),
static_cast<int>(s.size),
static_cast<int>(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<int>(((float)std::rand() / RAND_MAX * 2.0f - 1.0f) * mShakeIntensity * 15.0f);
camDY = static_cast<int>(((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<Uint8>(p.life * 255.0f)};
SDL_Rect pRect = {
static_cast<int>(p.x - p.size / 2),
static_cast<int>(p.y - p.size / 2),
static_cast<int>(p.size),
static_cast<int>(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<Uint8>(flash * 100), static_cast<Uint8>(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<Uint8>(180 + 75 * blink), static_cast<Uint8>(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<Uint8>(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<int>(camDX * static_cast<float>(mTargetW) / 800.0f),
static_cast<int>(camDY * static_cast<float>(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());
}
+74
View File
@@ -0,0 +1,74 @@
#pragma once
#include <SDL2/SDL.h>
#include <vector>
#include <string>
#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<Star> mStars;
std::vector<Particle> 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);
};
+802
View File
@@ -0,0 +1,802 @@
#include "Synth.hpp"
#include <cmath>
#include <cstdlib>
#include <algorithm>
#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<std::mutex> 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<std::mutex> 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<std::mutex> 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<std::mutex> 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<float> Synth::getVisualizerBuffer() {
std::lock_guard<std::mutex> lock(mMutex);
std::vector<float> 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;
}
+125
View File
@@ -0,0 +1,125 @@
#pragma once
#include <SDL2/SDL.h>
#include <mutex>
#include <vector>
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<float> 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);
};
+50
View File
@@ -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
+332
View File
@@ -0,0 +1,332 @@
#include <SDL2/SDL.h>
#include <iostream>
#include <chrono>
#ifdef MIYOO_BUILD
#include <cstdlib>
#include <fcntl.h>
#include <unistd.h>
#include <sys/ioctl.h>
#include <linux/fb.h>
#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<double>(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<float>((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;
}
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 9.3 KiB